取消未完成的Axios的异步请求

November 5, 2020 ... ☕️ 3 min read

开发时把网络速度调整到slow 3G,发现部分组件报了警告:

Warning: Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

造成这个问题对原因是,在已经unmounted的组件上,有异步回调函数触发,然后函数内部还有state的更新操作。解决问题也给的很清楚:在useEffect的清理函数(返回的函数)里,取消异步任务的订阅。

那么,对于使用Axios库的项目,如果是已经发起的ajax请求,如何手动终止呢?顺着文档,找到了cancelToken这个选项。

相关代码块

const CancelToken = axios.CancelToken;
// CancelToken.source()是工厂方法,返回一个包含token和cancel方法的对象
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
  }
});
// 手动取消请求
source.cancel('Operation canceled by the user.');

对于浏览器(区别于node),Axios创建的异步连接,本质上是新建了一个XMLHttpRequest,然后包装Promise。

// Axios > lib > adapters > xhr.js
module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    // ...
    var request = new XMLHttpRequest();
    // ...
    if (config.cancelToken) {
      // Handle cancellation
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }

        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
      });
    }
    // ...
    // Send the request
    request.send(requestData);
  }
}

cancelToken.source是个工厂函数,通过它,返回token实例和一个cancel处理函数。

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

可以看到,整体思路是在xhrAdapter里新建了一个XHR连接,然后通过config.cancelToken.promise.then的处理,来达到终止XHR连接(abort)的目的。

作为一个Promise,要触发then,这个Promise必须resolve,而这个resolve,则应该是可以由用户触发的,才能达到目的。

CancelToken这个构造函数,返回的实例,包含一个promise。

// Axios > lib > cancel > CancelToken.js
function CancelToken(executor) {
  // ...
  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);
  });
}

source()函数里,new CancelToken的时候,executor函数只有一个操作:cancel = c。

这里executor(function cancel(...) {...})),实际把整个函数赋值过去,即cancel = function cancel(...) {...})。然后把这个cancel函数,连同CancelToken实例,一起暴露给用户。即,必须在CancelToken实例上执行对应的cancel函数,才会起效。

总结

所以梳理一下执行流程就是:

cancel -> resolvePromise -> then -> request.abort()

cancel-axios-req CancelToken流程

由于每个axios.get/post都会生成一个对应的XHR连接,所以,每个连接都需要由source()工厂函数,生成新的token和对应的cancel,否则,使用相同的token,调用cancel时,会把几个连接一起停掉,这一点需要注意。

#react#axios

SideEffect is a blog for front-end web development.
Code by Axiu / rss