axios解析之cancelToken取消请求原理

6,716 阅读7分钟

axios 版本: 0.24.0

在「面试官:如何中断已发出去的请求」一文中,我们介绍了axios取消请求的两种方式。在本文中,我们将从源码的角度来解析cancelToken取消请求的原理。

CancelToken构造函数取消请求

通过调用CancelToken的构造函数来实现取消请求主要分为两步:

  1. 调用CancelToken构造函数创建cancelToken实例对象,并创建一个 cancel函数
  2. 调用cancel函数,取消请求

使用代码如下:

import axios from 'axios';

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    cancel = c;
  })
});

// 取消请求
cancel('Operation canceled by the user.');

那么,在调用CancelToken构造函数创建cancelToken实例对象时axios做了什么?在调用cancel函数时又是如何取消请求的?带着疑问,我们对axios的源码探究一番。

创建cancelToken对象

cancelToken实例对象是通过CancelToken构造函数创建的,CancelToken构造函数的定义在lib/cancel/CancelToken.js 文件中。

我们从cancelToken对象的创建过程来解析CancelToken构造函数。

1、定义静态变量resolvePromise

在CancelToken构造函数中,首先定义了一个resolvePromise的静态变量,该静态变量是一个promise对象,在将来可以执行取消请求的操作。

// lib/cancel/CancelToken.js

/**
 * 定义一个 promise 对象,该对象在将来可以执行取消请求
 * 当这个 promise 的状态为 已成功(fulfilled),就会触发取消请求的操作 (执行then函数)
 * 而执行resolve就能将promise的状态置为完成状态
 */
var resolvePromise;

2、初始化CancelToken实例的promise属性

调用new Promise()生成一个promise实例,并将这个promise实例赋值给CancelToken实例的promise属性,将来定义then函数就是通过这个promise属性得到一个新的promise。

在生成promise实例的时候,将Promise构造函数的resolve参数赋值给了静态变量resolvePromise,这样就可以在Promise构造函数外部执行resolve,在外部改变promise的状态。

// lib/cancel/CancelToken.js

// 生成一个Promise实例赋值给 CancelToken 实例的promise属性
// 这里生成Promise实例时将 resolvePromise 福赋值给了CancelToken 实例的promise属性
this.promise = new Promise(function promiseExecutor(resolve) {
  // 把 resolve 赋值给 resolvePromise,就是为了在这个 promise 外能执行 resolve 而改变这个promise的状态
  resolvePromise = resolve;
});

3、定义静态变量token

定义静态变量token,并将其赋值为CancelToken的实例,后面执行取消操作时的onCanceled函数和由用户定义的取消请求的原因等信息都将添加到该静态变量上。

// lib/cancel/CancelToken.js

// 将 CancelToken 实例赋值给 token
  var token = this;

4、执行executor函数

在实例化CancelToken构造函数时会传入executor函数,执行该函数,将内部定义的cancel函数传递给executor,即业务中调用的cancel就是CancelToken构造函数内部定义的cancel函数。

// lib/cancel/CancelToken.js

// 执行executor,这里定义的cancel函数即时业务中调用的cancel函数
executor(function cancel(message) {
  if (token.reason) {
    // Cancellation has already been requested
    return;
  }

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

5、定义onCanceled函数

onCanceled函数的定义在 lib/adapters/xhr.js 文件中,onCanceled是真正执行取消请求操作的函数。

// lib/adapters/xhr.js

if (config.cancelToken || config.signal) {
  // Handle cancellation
  // eslint-disable-next-line func-names
  // 定义onCanceled函数
  onCanceled = function (cancel) {
    if (!request) {
      return;
    }
    reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
    request.abort();
    request = null;
  };

  // 订阅 onCanceled函数
  // onCanceled 函数会被添加到CancelToken实例的 _listeners 数组中
  // 当执行取消请求操作时,从_listeners中取出onCanceled函数,然后执行
  config.cancelToken && config.cancelToken.subscribe(onCanceled);
  if (config.signal) {
    config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
  }
}

6、订阅onCanceled函数

订阅onCanceled函数时,调用CancelToken实例的subscribe方法,将onCanceled添加到CancelToken实例的_listeners属性里,当执行取消请求操作时,就从_listeners中取出onCanceled函数,执行该函数取消请求。

// lib/cancel/CancelToken.js

/**
* Subscribe to the cancel signal
*/

CancelToken.prototype.subscribe = function subscribe(listener) {
// 参数 listener 就是 onCanceled 函数
// this.reson 是取消请求的原因等信息。它是一个 Cancel实例对象
  
if (this.reason) {
  listener(this.reason);
  return;
}

// 将 onCanceled 添加到CancelToken实例的 _listeners 属性里
if (this._listeners) {
  this._listeners.push(listener);
} else {
  this._listeners = [listener];
}
};

7、流程图

执行 new CancelToken() 创建cancelToken实例对象的过程如下:

8、小结

在执行new CancelToken() 创建cancelToken实例对象的过程中,主要做了两件事:

  1. 创建一个Promise实例对象,并将这个对象赋值给CancelToken实例的promise属性,同时将Promise的resolve参数对外暴露,便于在外部调用resolve,改变promise的状态。

  2. 执行executor函数,将内部定义的cancel函数 (onCanceled函数) 作为参数传递给executor,并将cancel函数添加到cancelToken实例对象的_listeners属性里。

cancel取消请求

在创建cancelToken实例对象阶段执行executor函数时,会将内部定义的cancel函数传递给传入的executor函数。在业务中执行取消请求操作时,执行的就是内部定义的cancel函数。

1、执行cancel函数

在执行cancel函数时,首先实例化一个Cancel实例对象,将用户传入的消息(取消请求的原因等信息)添加到Cancel实例对象中,然后将这个Cancel实例对象添加得到cancelToken实例对象上,然后调用resolvePromise, 改变promise的状态。

// lib/cancel/CancelToken.js

// 执行 executor 函数,将 cancel 方法传入 executor
// executor 是实例化 CancelToken 时传入的函数
// cancel 方法调用 resolvePromise,即触发取消请求的操作
// cancel 方法就是CancelToken实例化时函数参数 executor 的 参数 c /// new CancelToken(function executor(c) {}
executor(function cancel(message) {
  // token 是 CancelToken 实例
  if (token.reason) {
    // Cancellation has already been requested
    return;
  }

  // 实例化Cancel,执行 cancel 时将用户传递的信息添加到  CancelToken 实例的 reason 属性上
  token.reason = new Cancel(message);
  // 这里执行的就是promise的resolve方法,改变状态
  resolvePromise(token.reason);
});

2、执行resolvePromise

这里的resolvePromise就是初始化CancelToken实例的promise属性时对外暴露的resolve参数,在外部执行resolve,将promise(CancelToken实例的promise)的状态转为fullfilled状态,然后进入promise的then回调中。

// lib/cancel/CancelToken.js

// 这里执行的就是promise的resolve方法,改变状态
resolvePromise(token.reason);

3、触发then回调

进入promise的then回调后,从CancelToken实例对象的_listeners里取出 onCanceled 函数,然后执行该函数。

// lib/cancel/CancelToken.js

// 这里的 cancel 参数是 Cancel 实例,即存储了用户取消请求时传入的信息
this.promise.then(function (cancel) {

  if (!token._listeners) return;

  var i;
  var l = token._listeners.length;

  for (i = 0; i < l; i++) {
    // 这里执行 onCanceled 函数
    token._listeners[i](cancel);
  }
  // 重置 _listeners
  token._listeners = null;
});

4、执行 onCanceled 函数

执行onCanceled函数的时候,会执行 XMLHttpRequest 的 abort 方法取消请求,也就是说,当用户调用cancel方法后,最终会执行abort方法取消请求,同时调用reject让外层的promise失败。

// lib/adapters/xhr.js

onCanceled = function(cancel) {
  if (!request) {
    return;
  }
  reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
  // 调用xhr的abort()取消请求
  request.abort();
  request = null;
};

5、流程图

执行cancel取消请求的过程如下:

6、小结

当用户调用内部对外暴露的cancel方法后,axios内部会执行resolvePromise,改变promise(CancelToken实例的promise)的状态,触发promise的then回调,然后执行onCanceled方法,在onCanceled中则调用XMLHttpRequest 的abort方法取消请求,同时调用reject让外层的promise失败。

CancelToken.souce 取消请求

CancelToken.source 是一个函数,执行const source = CancelToken.source();后会得到一个对象,它包含了一个token对象,即CancelToken实例对象,和一个cancel函数。因此CancelToken.source()函数的作用是生成token对象和取得cancel函数。token对象是用于配置给axios请求中的cancelToken属性,cancel函数是将来触发取消请求的函数。

// lib/cancel/CancelToken.js

/**
 * Returns an object that contains a new `CancelToken` and a function that, when called,
 * cancels the `CancelToken`.
 */
CancelToken.source = function source() {
  var cancel;
  // 实例化一个 CancelToken
  var token = new CancelToken(function executor(c) {
    // 参数 c 是CancelToken中给executor传入的cancel方法
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

从源码中可以看到,CancelToken.source其实就是axios在内部创建了一个CancelToken实例对象和取消请求的cancel函数,对token和cancel进行了封装。因此,CancelToken.source取消请求的原理与CancelToken构造函数取消请求的原理是一样的,在此不再赘述。