Axios源码阅读(三):取消请求
一、功能介绍
官方文档指出有2种方法可以取消请求,分别是cancelToken
和abortController
,下面是示例代码:
// 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();
通过文档描述和示例代码总结出以下功能点:
- 支持
cancelToken
取消请求,cancelToken
可以通过工厂函数产生,也可以通过构造函数生成; - 支持
Fetch API
的AbortController
取消请求; - 一个
token/signal
可以取消多个请求,一个请求也可同时使用token/signal
; - 如果在开始
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
是基于已撤销的提案,所以Axios
从v0.22.2
使用AbortController
,为了兼容以前的代码,现在库中仍然保留了相关代码,在原来使用CancelToken
的地方使用了||
确保两者都起作用。
2.4 多个请求观察一个对象
能实现这个功能是因为代码采取了良好的设计。
基于 CancelToken
每个token
实例都有一个_listeners
数组,当每个请求的adapter
执行的时候都会往_listeners
压入一个观察回调,当token.promise
凝固后就会执行所有的观察回调。
基于 AbortController
使用AbortController
就更简单了,这个相当于是原生实现观察者模式。
三、总结
个人感觉cancellation
这部分代码是最绕的,又因为各种异步代码,所以有些地方光凭看很难知道到底是怎么执行的,所以我们不妨运行Axios
提供的测试代码,然后再浏览器中本地调试,通过打断点查看执行请情况。
通过这次源码阅读,我了解了:
-
Axios
是如何取消请求的; - 可以使用
token.promise.then
添加观察者,也可以使用token.subscribe
。不同的是前者添加的观察者是异步执行,而后者添加的观察者是同步执行; -
AbortController
对象; - 使用
function
作为参数访问函数内部变量。