axios源码学习(二):axios怎么实现请求取消?

673 阅读5分钟

axios怎么实现请求取消?其实我在一开始用axios的cancelToken的时候,还是一脸茫然的,因为从操作来看,取消请求其实是一个操作,如果按照我们平时的思维,可能会考虑为把它写成Axios上的一个方法,直接调用就好了,毕竟在调起ajax的open()之后,是可以直接调abort()的。但是在调起ajax之前呢?那加一个请求状态来处理?然后取消请求之后后面的逻辑怎么走?好像多了不少逻辑。

我们先在看看cancelToken是怎么用的吧。

怎么操作的

这里直接用官方文档的用例来演示。

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
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');

上面这个例子可以看到,如果请求需要取消的话,可以在发请求的时候设置cancelToken这个配置,值是调用CancelToken.source()获得的对象中的token。然后这个token同时可以提供给不同的请求中。当需要取消请求时,可以调source.cancel()方法,这个方法可传入一个参数,这个参数最终会出现在被取消请求的catch中的error对象中。所以拿到相同token的请求都会被取消。

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();

上面这个用例是直接new一个CancelToken的实例,传入一个executor()方法,这个方法会得到一个c方法,我们可以在外部拿到这个c,然后在需要取消请求的时候调用它。

实现源码分析

在看上面两个用例的时候有没有观察到一些细节:如果有了解过Promise手写实现,那么第二个用例new一个CancelToken的实例的代码会看着很眼熟。我们改改看:

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new Promise(function executor(resolve, reject) {
    // An executor function receives a cancel function as a parameter
    cancel = resolve; // reject参数弃去
  })
});

// cancel the request
cancel(); // 相当于调resolve()

看起来像是那么回事。如果在结合第一个用例提及的调用cancel()方法可以把使用相同token的请求取消掉,这里的token如果就是Promise实例的话,那么调cancel()方法就会让tokenthen方法执行了。

好了,其实内部确实是基于Promise来实现的。

new CancelToken(executor)时,内部就new一个promise实例,然后在将resolve保存起来。对于用户传入的executor方法,调这个方法并传入一个cancel()方法,这个方法接收一个message的参数,内部会用它去new一个Cancel实例,并调用resolve方法。这个Cancel实例其实只是用来保存取消信息。

/**
 * A `CancelToken` is an object that can be used to request cancellation of an operation.
 *
 * @class
 * @param {Function} executor The executor function.
 * 
 * 支持通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token
 * 相当于CancelToken实例会给executor函数传入一个cancel的方法,这个方法可以abort请求
 */
function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  /**
  * 定义一个将来能执行取消请求的promise对象,当这个promise的状态为完成时(fullfilled),
  * 就会触发取消请求的操作(执行then函数)。而执行resolve就能将promise的状态置为完成状态。
  * 这里把resolve赋值给resolvePromise,就是为了在这个promise外能执行resolve而改变这个promise的状态
  * 注意这个promise对象被赋值给CancelToken实例的属性promise,将来定义then函数就是通过这个属性得到promise
  */
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  /**
    * 将CancelToken实例赋值给token
    * 执行executor函数,将cancel方法传入executor,
    * cancel方法可调用resolvePromise方法,即触发取消请求的操作
    * 
    * Cancel实例其实是保存了CancelToken实例的取消状态
    * 如果多次调用cancel方法,内部只会执行一次resolvePromise方法
    */
  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

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

那还是看不出怎么取消请求的呢。这个就得到适配器中找源码了。这里我们找到/adapters/xhr.js,这里有判断是否有设置cancelToken,如果有,则给这个cancelToken.promisethen,在内部调abort()方法取消请求,并调reject()结束请求Promise,直接进入失败态,到响应拦截器是失败拦截函数中。

// 如果有设置cancelToken,就设置then中的处理
if (config.cancelToken) {
  // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    // 如果请求已经清空,就不再处理
    if (!request) {
      return;
    }

    // 取消请求,然后reject调Promise,再清空request
    request.abort();
    reject(cancel);
    // Clean up request
    request = null;
  });
}

回过头来,CancelToken还实现了source()方法,这个方法也是new一个CancelToken实例,然后把实例作为tokencancel()方法作为cancel,整合成一个对象返回出去。

/**
 * Returns an object that contains a new `CancelToken` and a function that, when called,
 * cancels the `CancelToken`.
 * 这个方法方便外部一次性拿到token跟cancel
 * 然后把token设置给请求的cancelToken配置中,cancel用来需要取消请求时调用
 */
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

结尾

关于axios请求取消的源码分析就到这结束了。下一篇会介绍一下axios的拦截器。