Axios
本身是一个返回值为 promise
的函数。
而在这种场景下,中止 promise
的方式有两种:
cancelToken
,利用的是CancelToken
,更详细可以说是利用promise状态机。signal
,利用的是AbortController
,更详细可以说是利用abort事件监听。
虽然在当前较新版本的 axios
中, CancelToken
已经不被推荐使用。
但我们本节依然要重点梳理 CancelToken
的实现机制,以更好的理解 promise
状态机。
关于后者 signal
的相关介绍,实际上是借鉴了 fetch
的中止请求方式,可以参考 AbortController。
TIP
关于 promise
的中止,有两个不同的概念:
- 封装一个支持中止内部
promise
的函数。 - 封装一个支持可中止的
promise
。
笔者尝试了一下,前者即是 axios
中止的相关实现,而后者较难实现,或者无法实现(我们更多的是直接在外部作用域 reject
,从而强制改变 promise
状态)。
js
class CancelToken {
constructor(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
let resolvePromise;
/*
通过两次闭包的形式,将resolve的控制权抛给了外部使用者。当用户调用了cancel()方法之后,resolve方法被触发,此时then函数回调触发。
这里之所以要额外使用Promise链,而不是直接将执行listeners的方法抛出,主要是因为promise.then微任务的执行机制。
*/
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
const token = this;
this.promise.then(cancel => {
if (!token._listeners) return;
let i = token._listeners.length;
while (i-- > 0) {
token._listeners[i](cancel);
}
token._listeners = null;
});
/*
这一段代码实际是对 config.cancelToken 的版本向下兼容。
因为原始使用方式是这样的config.cancelToken.promise.then()。
在现有Axios的源码逻辑,并不会触发此处。会触发上面的then方法
*/
this.promise.then = onfulfilled => {
let _resolve;
const promise = new Promise(resolve => {
/*
这里的订阅,笔者认为并没有作用。因为当进行到这步时,外层promise已经是resolved状态了。
因此,即使在此订阅后,再手动触发 `token.cancel`,也不会执行所有的listeners了。
*/
token.subscribe(resolve);
_resolve = resolve;
}).then(onfulfilled);
promise.cancel = function reject() {
token.unsubscribe(_resolve);
};
return promise;
};
executor(function cancel(message, config, request) {
if (token.reason) {
return;
}
// 当取消时,会给实例token添加reason属性,也就是说,我们可以通过token.reason是否存在,来判断取消操作是否已经触发。
token.reason = new CanceledError(message, config, request);
resolvePromise(token.reason);
});
}
// 如果已经取消了,则直接抛出错误
throwIfRequested() {
if (this.reason) {
throw this.reason;
}
}
// 订阅
subscribe(listener) {
if (this.reason) {
listener(this.reason);
return;
}
if (this._listeners) {
this._listeners.push(listener);
} else {
this._listeners = [listener];
}
}
// 取消订阅
unsubscribe(listener) {
if (!this._listeners) {
return;
}
const index = this._listeners.indexOf(listener);
if (index !== -1) {
this._listeners.splice(index, 1);
}
}
// 静态方法
static source() {
let cancel;
const token = new CancelToken(function executor(c) {
cancel = c;
});
// 返回token和cancel
return {
token,
cancel
};
}
}
从上述代码中,可以看出 CancelToken
利用了订阅/发布设计模式。
但值得一提的是,截止到目前为止,CancelToken
的改动一共涉及了两版:
在版本 2
中才修改为了订阅/发布设计模式。在发布记录中,可以看到一部分目的是为了修复内存泄漏,但这个笔者不太确定。
TIP
这部分的订阅/发布模式,是基于 promise.then
的。并不能是同步调用。
之所以这样设计,是因为微任务的执行时机和执行机制。
譬如 CancelToken
是在 adapter
中调用的,而 adapater
以及 interceptor
都是在 promise.then
链中执行的。