我就不信我搞不明白取消请求

简单的封装一个 ajax 请求

类似这样 ajax 请求的封装,promise 的状态扭转取决于当次 ajax 的响应状态来决定

状态的扭转的逻辑在 exec 函数体内,是被动的发生

js 复制代码
const request = () => {
  const exec = (resolve, reject) => {
    const xhr = new XMLHttpRequest();

    xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/1");

    xhr.onload = function () {
      if (xhr.readyState === 4) {
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
          resolve(JSON.parse(xhr.responseText));
        } else {
          reject(xhr.status);
        }
      }
    };

    xhr.addEventListener("loadstart", (e) => {
      console.log("请求开始", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("loadend", (e) => {
      console.log("请求结束", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("load", (e) => {
      console.log("请求中", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("abort", (e) => {
      console.log("请求取消", e.type);
    });
  };

  return new Promise(exec);
};

request()
  .then((res) => {
    console.log("请求成功", res);
  })
  .catch((err) => {
    console.log("请求失败", err);
  });

主动修改 promise

务必记着这个场景

可是某些场景我们需要 在 exec 函数 体外来决定这个 promise 的状态

所以可以把这两个参数赋值给外部的变量来 在 exec 函数外部 动态决定 promise 的状态

js 复制代码
let resolvePromise, rejectPromise;

const exec = (resolve, reject) => {
  resolvePromise = resolve;
  rejectPromise = reject;
};

const p = new Promise(execuoter);

p.then((res) => {
  console.log("1", res);
}).catch((err) => {
  console.log("2", err);
});

// p 的状态就取决于何时调用 resolvePromise
setTimeout(() => {
  // exec 外部
  resolvePromise("aaa");
}, 1000);

实现一个请求取消

xhr 对象提供了取消请求的方法 叫做 abort

所以要实现的核心就是何时调用 abort

js 复制代码
const request = () => {
  // 取消的发布订阅
  // 暂时不考虑其他边界场景哈
  const myAbortEvent = () => {
    const abort = [];
    return {
      on: (fn) => {
        abort.push(fn);
      },
      emit: (data) => {
        abort.forEach((h) => h(data));
      },
    };
  };

  const ev = myAbortEvent();

  // 这里要使用外部变量的原因就是目前我们请求的 promise 的状态变更的渠道又多了一个
  // 之前很单一就是依赖请求的响应结果
  // 但是现在我们的场景多了一个,那就是取消的场景
  // 取消的场景会是异步也会是同步,所以我们需要使用外部修改的方式来实现

  let rp, rj;

  const exec = (resolve, reject) => {
    let xhr = new XMLHttpRequest();

    rp = resolve;

    rj = reject;

    xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/1");

    xhr.send();

    xhr.onload = function () {
      if (xhr.readyState === 4) {
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
          rp(JSON.parse(xhr.responseText));
        } else {
          rj(xhr.status);
        }
      }
    };

    xhr.addEventListener("loadstart", (e) => {
      console.log("请求开始", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("loadend", (e) => {
      console.log("请求结束", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("load", (e) => {
      console.log("请求中", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("abort", (e) => {
      console.log("请求取消", e.type);
    });

    // 调用的时机取决于外部的调用
    // 也就是说调用的场景可能是同步发生也可能异步发生
    // 解决这样的场景的方案那不就是发布订阅么
    // 那就内部维护一个只处理取消的简单的发布订阅场景 myAbortEvent

    ev.on((msg) => {
      xhr.abort();
      rj(msg);
      xhr = null;
    });
  };

  const send = new Promise(exec);

  return {
    send: new Promise(exec),
    ev: ev,
  };
};

const { send, ev } = request();

send
  .then((res) => {
    console.log("请求成功", res);
  })
  .catch((err) => {
    console.log("请求失败", err);
  });

ev.emit("取消请求");

优化取消的逻辑

之前实现了请求的取消,但是有很多的缺陷

比如只能取消单个请求,而且用法丑陋

要想满足取消多个请求的场景,那就要考虑如何用一个状态来控制多个请求的 promise 的状态

先推翻下之前的方式,先设计下用法

给 request 函数增加传递一个 cancel 参数,相当于我们把一个统一的状态传递给了多个请求函数

现在的节奏是倒着往里推,用法推实现

js 复制代码
const source = "未知的有特殊能力的对象,有 token 和 cancel";

let request1 = request({
  cancel: source.token,
});
let request2 = request({
  cancel: source.token,
});

source.cancel("取消请求");

开始设计这个特殊能力对象

js 复制代码
// 这样的方式满足不了需求
// 需求 执行 cancel 修改 source.token 状态
// request 内部订阅这个状态,状态变更了就执行 abort

//   const source = {
//     cancel: (msg) => { // 执行 cancel 后就能修改 token 的状态 },
//     token: new Promise(() => {}),
//   };

// 直接换函数,简单的来个自执行
const source = (function () {
  let cancel;
  let token = new Promise((resolve, reject) => {
    cancel = resolve;
  });
  return {
    token: token,
    cancel: cancel,
  };
})();

// 用法
let request1 = request({
  cancel: source.token,
});

let request2 = request({
  cancel: source.token,
});

request1
  .then((res) => {
    console.log("request1", res);
  })
  .catch((err) => {
    console.log("request1 err", err);
  });

request2
  .then((res) => {
    console.log("request2", res);
  })
  .catch((err) => {
    console.log("request2 err", err);
  });

source.cancel("取消所有请求");

好,开始修改 request 内部方法

js 复制代码
const request = (config = {}) => {
  let rp, rj;
  const exec = (resolve, reject) => {
    let xhr = new XMLHttpRequest();

    rp = resolve;

    rj = reject;

    xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/1");

    xhr.send();

    // 每个 request 返回的 promise 内部对这个 cancel 状态进行监听
    // 如果状态扭转了就执行 abort
    if (config.cancel) {
      config.cancel.then((res) => {
        xhr.abort();
        rj(res);
        xhr = null;
      });
    }

    xhr.onload = function () {
      if (xhr.readyState === 4) {
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
          rp(JSON.parse(xhr.responseText));
        } else {
          rj(xhr.status);
        }
      }
    };

    xhr.addEventListener("loadstart", (e) => {
      console.log("请求开始", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("loadend", (e) => {
      console.log("请求结束", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("load", (e) => {
      console.log("请求中", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("abort", (e) => {
      console.log("请求取消", e.type);
    });
  };

  return new Promise(exec);
};

整合下代码

js 复制代码
const request = (config = {}) => {
  let rp, rj;
  const exec = (resolve, reject) => {
    let xhr = new XMLHttpRequest();

    rp = resolve;

    rj = reject;

    xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/1");

    xhr.send();

    if (config.cancel) {
      config.cancel.then((res) => {
        xhr.abort();
        rj(res);
        xhr = null;
      });
    }

    xhr.onload = function () {
      if (xhr.readyState === 4) {
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
          rp(JSON.parse(xhr.responseText));
        } else {
          rj(xhr.status);
        }
      }
    };

    xhr.addEventListener("loadstart", (e) => {
      console.log("请求开始", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("loadend", (e) => {
      console.log("请求结束", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("load", (e) => {
      console.log("请求中", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("abort", (e) => {
      console.log("请求取消", e.type);
    });
  };

  return new Promise(exec);
};

const source = (function () {
  let cancel;
  let token = new Promise((resolve, reject) => {
    cancel = resolve;
  });
  return {
    token: token,
    cancel: cancel,
  };
})();

let request1 = request({
  cancel: source.token,
});

let request2 = request({
  cancel: source.token,
});

request1
  .then((res) => {
    console.log("request1", res);
  })
  .catch((err) => {
    console.log("request1 err", err);
  });

request2
  .then((res) => {
    console.log("request2", res);
  })
  .catch((err) => {
    console.log("request2 err", err);
  });

source.cancel("取消所有请求");

现在就实现了取消多个请求的场景 并且还优化了我们的用法 想取消哪个就在哪个请求里传递这个状态就可以了

AbortController 取消控制器

js 提供了一个控制器对象,可以按照需求中止一个或者多个请求

但是这个只对 fetch 这种现代化的请求方案有用

我想我们写的请求里也能支持取消控制器的方式来取消请求

简单的了解下 AbortController

js 复制代码
const ab = new AbortController();

console.log("控制器的是否是取消状态", ab.signal.aborted);

ab.abort(); // 用来修改 ab.signal.aborted 状态

// ab.signal 可以绑定一个 abort 事件来查询当前实例的取消状态,用这个钩子可以区分出同步和异步的场景
ab.signal.addEventListener("abort", () => {
  console.log("取消");
});

开始扩展支持这种取消请求的方式在我们写好的基础上

js 复制代码
const request = (config = {}) => {
  let rp, rj;
  const exec = (resolve, reject) => {
    let xhr = new XMLHttpRequest();

    rp = resolve;

    rj = reject;

    xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/1");

    xhr.send();

    if (config.cancel) {
      config.cancel.then((res) => {
        xhr.abort();
        rj(res);
        xhr = null;
      });
    }

    // 判断下是否传递了 config.signal
    if (config.signal) {
      // 查询 config.signal 的取消状态
      if (config.signal.aborted) {
        xhr.abort();
        rj("请求取消");
        xhr = null;
      } else {
        // 监听 abort 事件
        config.signal.addEventListener("abort", () => {
          // 触发了取消事件就执行取消逻辑
          if (config.signal.aborted) {
            xhr.abort();
            rj(config.signal.reason);
            xhr = null;
          }
        });
      }
    }

    xhr.onload = function () {
      if (xhr.readyState === 4) {
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
          rp(JSON.parse(xhr.responseText));
        } else {
          rj(xhr.status);
        }
      }
    };

    xhr.addEventListener("loadstart", (e) => {
      console.log("请求开始", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("loadend", (e) => {
      console.log("请求结束", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("load", (e) => {
      console.log("请求中", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("abort", (e) => {
      console.log("请求取消", e.type);
    });
  };

  return new Promise(exec);
};

const source = new AbortController();

let request1 = request({
  signal: source.signal,
});

let request2 = request({
  signal: source.signal,
});

request1
  .then((res) => {
    console.log("request1", res);
  })
  .catch((err) => {
    console.log("request1 err", err);
  });

request2
  .then((res) => {
    console.log("request2", res);
  })
  .catch((err) => {
    console.log("request2 err", err);
  });

source.abort("取消所有请求");

基本上实现了,但是代码有点些许重复

提取下可复用代码

js 复制代码
const request = (config = {}) => {
  let rp, rj;
  const exec = (resolve, reject) => {
    let xhr = new XMLHttpRequest();

    rp = resolve;

    rj = reject;

    xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/1");

    xhr.send();

    let cb;
    if (config.cancel || config.signal) {
      cb = (msg) => {
        xhr.abort();
        rj(msg);
        xhr = null;
      };
    }

    if (config.cancel) {
      config.cancel.then(cb);
    }

    if (config.signal) {
      config.signal.aborted
        ? cb("请求取消")
        : config.signal.addEventListener("abort", () => {
            if (config.signal.aborted) {
              cb(config.signal.reason);
            }
          });
    }

    xhr.onload = function () {
      if (xhr.readyState === 4) {
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
          rp(JSON.parse(xhr.responseText));
        } else {
          rj(xhr.status);
        }
      }
    };

    xhr.addEventListener("loadstart", (e) => {
      console.log("请求开始", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("loadend", (e) => {
      console.log("请求结束", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("load", (e) => {
      console.log("请求中", e.type + " === " + e.loaded);
    });

    xhr.addEventListener("abort", (e) => {
      console.log("请求取消", e.type);
    });
  };

  return new Promise(exec);
};

const source = new AbortController();

let request1 = request({
  signal: source.signal,
});

let request2 = request({
  signal: source.signal,
});

request1
  .then((res) => {
    console.log("request1", res);
  })
  .catch((err) => {
    console.log("request1 err", err);
  });

request2
  .then((res) => {
    console.log("request2", res);
  })
  .catch((err) => {
    console.log("request2 err", err);
  });

source.abort("取消所有请求");

回答如何实现取消请求

使用的请求的技术方案不同,取消的方式不同

fetch 取消

通过 AbortController 控制器取消

js 复制代码
const controller = new AbortController();
const signal = controller.signal;
fetch("http://www.baidu.com", { signal })
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.log(err);
  });

controller.abort();

axios 也就是 xhr 的取消

方案 1

通过 CancelToken 类创建一个取消控制器

js 复制代码
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios
  .get("http://www.baidu.com", {
    cancelToken: source.token,
  })
  .catch(function (thrown) {
    if (axios.isCancel(thrown)) {
      console.log("Request canceled", thrown.message);
    } else {
      // 处理错误
    }
  });
source.cancel("取消");

方案 2

通过 AbortController 控制器取消

js 复制代码
const controller = new AbortController();
axios
  .get("http://www.baidu.com", {
    signal: controller.signal,
  })
  .catch(function (thrown) {
    // 处理错误
  });
controller.abort("取消");

原理,我们已经敲过了啊

相关推荐
耶啵奶膘6 分钟前
css——width: fit-content 宽度、自适应
前端·css
OEC小胖胖7 分钟前
前端框架状态管理对比:Redux、MobX、Vuex 等的优劣与选择
前端·前端框架·web
字节架构前端1 小时前
k8s场景下的指标监控体系构建——Prometheus 简介
前端·架构
奕羽晨1 小时前
关于CSS的一些读书笔记
前端·css
Poetry2371 小时前
大屏数据可视化适配方案
前端
遂心_1 小时前
用React Hooks + Stylus打造文艺范的Todo应用
前端·javascript·react.js
轻语呢喃1 小时前
<a href=‘ ./XXX ’>,<a href="#XXX">,<Link to="/XXX">本质与区别
前端·react.js·html
用户3802258598241 小时前
vue3源码解析:watch的实现
前端·vue.js·源码
F2E_Zhangmo2 小时前
第一章 uniapp实现兼容多端的树状族谱关系图,创建可缩放移动区域
前端·javascript·uni-app