Skip to content

如何取消 Web 请求 #128

Description

@tcatche

本文博客地址:如何取消 Web 请求.

在有些场景下可能需要用到取消请求的操作,比如在同一时刻触发多次请求,只保留最新的请求;比如表单提交中关闭页面等行为,本文加少一些常用的触发 Web 请求的方式如何取消。

AbortControllor

AbortController 接口表示一个控制器对象,该对象允许您在需要时中止一个或多个 Web 请求。

AbortController 分别有一个属性 AbortController.signal 和一个方法 AbortController.abort,他们结合起来可以用来终止 Web 请求。

它的使用过程如下:

首先创建一个 AbortController 对象 controller,然后在调用 fetch 时,传入 controller.signal 作为选项,此时控制器和请求被关联在一起,使用控制器的 controller.abort 方法,即刻终止此请求。

const controller = new AbortController();

function download() {
  return fetch(url, { signal: controller.signal });
}

function abort() {
  controller.abort();
}

注意,当 controller.abort 被调用时, fetch 返回的 promise 会进入 reject 状态并返回一个名为 AbortError 的 DOMException。

AbortController.signal 搭配 AbortController.abort 还可以用来响应一些取消类的行为:

const controller = new AbortController();
controller.signal.addEventListener('abort', (err) => {
  console.log('aborted!');
});
controller.abort();

// 输出
aborted!

注意,AbortControllor 只对浏览器原生的 fetch 生效,在日常开发中经常使用其它的工具如 axios,下一节看一下 axios 怎么取消。

axios 请求取消

axios 提供了 cancelToken 配置项可以用来取消其请求,其使用和 AbortControllor 很相似,看下面的例子:

const cancelToken = axios.CancelToken;
const source = cancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

// cancel the request (the message parameter is optional)
source.cancel();

也可以给 CancelToken 传入一个执行器来执行:

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // An executor function receives a cancel function as a parameter
    cancel = c;
  })
});

// cancel the request
cancel();

都写到这里了,顺便看看 axios 内部是怎么实现取消的:

首先看 cancelToken 相关的代码:

// lib/adapters/xhr.js

if (config.cancelToken) {
  // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }

    request.abort();
    reject(cancel);
    // Clean up request
    request = null;
  });
}

可以看到当 cancelToken.promise 调用 resolve 时进行了取消,结合它的使用不难猜测 cancelToken.source().cancel 和 传入 CancelToken executor 返回给用户的方法一定用来是 resolve cancelToken.promise 的,接着找相关代码:

// lib/cancel/CancelToken.js
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

从上面看出 axios 的第一个示例 cancelToken.source 方法就是是调用了 CancelToken 构造器,也就是上面 axios 示例的第二种写法,后面就没必要看了,继续找构造器相关的代码就知道做了什么:

// lib/cancel/CancelToken.js
function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

很明显可以看出 this.promise 就是 config.cancelToken.promise.then,而 executor 就是 new CancelToken 时传入的方法,最终暴露给外面了 function cancel 这个方法,而这个方法就是用来 config.cancelToken.promise,和前面的猜测一致,不在多说。

同样的,使用同一个 cancelToken 也可以取消多个请求。

中止 Promise

Promise 也可以手动中止,只需要在创建 Promise 时,对外暴露 reject

let abort;
let p = new Promise((resovle, reject) => {
  abort = err => reject(err);
  // do sth;
});

abort();

XHR 请求取消

XHR 原生提供了 abort 方法,可以直接取消:

let xhr = new XMLHttpRequest();
xhr.onerror = (err) => console.log(err);
xhr.onabort = () => console.log('aborted!');
xhr.open('get', 'http://baidu.com');
xhr.send();

setTimeout(() => {
  xhr.abort();
}, 100);

注意,和其他不同的是,xhr 取消不会触发 error 事件,而是触发 abort 事件。

参考

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions