Axios源码阅读(三):取消请求

2021-12-02  本文已影响0人  前端艾希

一、功能介绍

官方文档指出有2种方法可以取消请求,分别是cancelTokenabortController,下面是示例代码:

// method 1
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
});

source.cancel('Operation canceled by the user.');

// method 2
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    cancel = c;
  })
});

cancel();

// method 3
const controller = new AbortController();
axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});

controller.abort();

通过文档描述和示例代码总结出以下功能点:

  1. 支持cancelToken取消请求,cancelToken可以通过工厂函数产生,也可以通过构造函数生成;
  2. 支持Fetch APIAbortController取消请求;
  3. 一个token/signal可以取消多个请求,一个请求也可同时使用token/signal
  4. 如果在开始axios request之前执行了取消请求,则并不会发出真实的请求(见Cancellation最后一个Note);

二、源码阅读

通过阅读源码逐个了解上述功能是如何实现的。

2.1 cancelToken 取消请求

通过搜索找到axios.CancelToken = require('./cancel/CancelToken'),于是打开该文件,找到CancelToken构造函数后,发现这个函数非常的绕,这里把代码结构稍微整理下:

传送门:./lib/cancel/CancelToken.js

var Cancel = require('./Cancel');

/**
 * A `CancelToken` is an object that can be used to request cancellation of an operation.
 *
 * @class
 * @param {Function} executor The executor function.
 */
function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }
  var token = this; // 把 this 保存在局部变量中

  // snippet 1
  // 给实例添加“promise”属性
  
  // snippet 2 
  // this.promise resolved 后,call所有的观察者,并且清空观察者队列
  
  // snippet 3
  // 重写 this.promise.then 方法,在 then 里面添加了订阅的代码
  
  // snippet 4
  // 执行构造函数传入的 executor,参数为一个改变 this.promise 状态的回调函数
}

上面的代码是CancelToken构造函数,这个函数实现的非常巧妙,我们逐一分析。

snippet 1

var resolvePromise;
// 给实例添加 promise 属性,并且把 resolve 保存在局部变量中
this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
});

这里把resolve保存在局部变量中为一个关键步骤,相当于保存了一把“钥匙”,以后可以随时用这把钥匙改变this.promise的状态继而引发观察者的action

snippet 2

this.promise.then(function(cancel) {
    // 通过上下文分析,这里传入的 cancel 为取消请求的原因对象,例如:{message: 'reason'}
    if (!token._listeners) return;

    var i;
    var l = token._listeners.length;
    // 将观察者队列清空
    for (i = 0; i < l; i++) {
      token._listeners[i](cancel);
    }
    token._listeners = null;
});

这里代码的功能是在请求被取消之后,执行所有之前所有订阅“取消”状态的方法,这里的代码也很关键,因为在xhr.js中需要根据该状态去执行xhr.abort,代码如下:

if (config.cancelToken || config.signal) {
    // Handle cancellation
    // eslint-disable-next-line func-names
    onCanceled = function(cancel) {
    if (!request) {
        return;
    }
    reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
        request.abort();
        request = null;
    };
    // 这里 onCanceled 订阅了取消事件
    config.cancelToken && config.cancelToken.subscribe(onCanceled);
    if (config.signal) {
        config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
    }
}

上面的代码在./lib/adapters/xhr.js中,cancelToken.subscribe方法会把onCanceled放到cancelToken实例的_listeners中。这里使用了观察者模式,使用户可以很方便的添加取消事件的回调。

snippet 3

this.promise.then = function(onfulfilled) {
    var _resolve;
    // eslint-disable-next-line func-names
    var promise = new Promise(function(resolve) {
        token.subscribe(resolve);
        _resolve = resolve;
    }).then(onfulfilled);

    promise.cancel = function reject() {
        token.unsubscribe(_resolve);
    };

    return promise;
};

这里的代码重写了token.promise.then方法,当调用者后续调用then方法时,添加的方法可以直接添加到实例的_listeners数组中,这个和直接调用subscribe的区别是,这样订阅的方法是异步执行的。

snippet 4

// 执行构造函数传入的 function,并且传入一个 cancel 方法
executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

    token.reason = new Cancel(message);
    // 改变 token.promise 的状态
    resolvePromise(token.reason);
});

在构造函数CancelToken执行时就执行传入的function,我认为这样做主要是为了在构造函数内对外暴露一个接口,可以通过这个方法访问构造函数内部变量。也是因为这些代码,所以支持config.cancelToken = new CancelToken

CancelToken.source

示例代码中提到使用source.cancel取消请求,这里看下这个功能是如何实现的:

/**
 * Returns an object that contains a new `CancelToken` and a function that, when called,
 * cancels the `CancelToken`.
 */
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

可以看到CancelToken.source返回一个对象,cancel就是在CancelToken构造函数内executor执行时传入的参数。

2.2 AbortController 取消请求

在这之前,我对AbortController没什么了解,所以先看下MDN-AbortController。看完后,我将其简单理解为abort事件控制器,因为其只支持abort事件,我们通过观察实例上的signal状态便能知道请求是否被取消。我们找到相关代码如下:

传送门:./lib/adapters/xhr.js

onCanceled = function(cancel) {
    if (!request) {
        return;
    }
    reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
    request.abort();
    request = null;
 };
if (config.signal) {
    config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}

xhr.send前监听了signal的状态,我们调用controller.abort后,执行onCanceled取消xhr请求。

2.3 一个请求订阅多个事件

因为CancelToken是基于已撤销的提案,所以Axiosv0.22.2使用AbortController,为了兼容以前的代码,现在库中仍然保留了相关代码,在原来使用CancelToken的地方使用了||确保两者都起作用。

2.4 多个请求观察一个对象

能实现这个功能是因为代码采取了良好的设计。

基于 CancelToken

每个token实例都有一个_listeners数组,当每个请求的adapter执行的时候都会往_listeners压入一个观察回调,当token.promise凝固后就会执行所有的观察回调。

基于 AbortController

使用AbortController就更简单了,这个相当于是原生实现观察者模式。

三、总结

个人感觉cancellation这部分代码是最绕的,又因为各种异步代码,所以有些地方光凭看很难知道到底是怎么执行的,所以我们不妨运行Axios提供的测试代码,然后再浏览器中本地调试,通过打断点查看执行请情况。

通过这次源码阅读,我了解了:

  1. Axios是如何取消请求的;
  2. 可以使用token.promise.then添加观察者,也可以使用token.subscribe。不同的是前者添加的观察者是异步执行,而后者添加的观察者是同步执行;
  3. AbortController对象;
  4. 使用function作为参数访问函数内部变量。
上一篇下一篇

猜你喜欢

热点阅读