1 前言
本文主要介绍axios是如何设计取消请求的。
如果您对axios请求整体流程还不太熟悉,建议先大致浏览一下该篇文章:
2 取消请求的思路
取消请求主要分为下面三种思路:
- 发送请求前取消
- 发送请求过程中取消
- 请求接收后取消
其中第一点和第三点比较好实现,只要在请求前和请求后判断是否取消了就行,如果取消,就抛出异常。
第二点Axios
使用类似观察者的模式,在请求前先注册好请求取消事件(abort
),一旦在请求过程中用户取消了请求,就会调用abort
,实现请求的取消。
3 取消请求的流程
axios
提供两种方式给用户,用来取消请求,分别是CancelToken
和AbortController
形式,我们先来看CancelToken
的设计和实现,使用AbortController
的原理其实与CancelToken
是一样的,只不过会更加简便。
3.1 CancelToken
CancelToken
的静态方法CancelToken.source
返回了两个东西:分别是token
和cancel
方法,先记住这两个东西的用途,后面会在源码中分析。
3.2 取消请求的流程
可以看到,流程里,axios
分别在请求前
、请求中
、请求后
进行了是否取消的判断。
这也就意味着,当我们在请求拦截器中调用cancel
方法,axios
会识别到,抛出异常。
如果我们在返回拦截器中调用cancel
方法,axios
就识别不到了,当然如果真要这么用,也不是不行,可以自己去判断token
的状态,进行处理。
4 CancelToken.js
接下来我们看看CancelToken
是怎么实现的吧:
// 用来取消请求的类
class CancelToken {
// 构造函数接收一个executor函数,
// 提前说明:该函数的回调参数为一个cancel函数
// 在下面可以看到
// 执行了该cancel函数之后,就说明这个Token被取消了
constructor(executor) {
// executor必须为方法
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
let resolvePromise;
// 定义一个promise,
// 将promise的resolve取出来
// 这样就可以在其他地方更改promise的状态了
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
const token = this;
// 如果取消了,执行监听函数
// 也就是通过token.subscripbe收集的订阅函数
this.promise.then((cancel) => {
if (!token._listeners) return;
let i = token._listeners.length;
while (i-- > 0) {
token._listeners[i](cancel);
}
token._listeners = null;
});
// 修改默认的promise.then方法,
this.promise.then = (onfulfilled) => {
let _resolve;
// 新建一个promise,
// 订阅函数
const promise = new Promise((resolve) => {
token.subscribe(resolve);
_resolve = resolve;
}).then(onfulfilled);
// 增加cancel函数
// 取消订阅
promise.cancel = function reject() {
token.unsubscribe(_resolve);
};
// 返回的是新的promise
return promise;
};
// 关键
// 执行传入的executor函数,
// 回调参数是一个cancel函数
// 该函数可以用来取消
executor(function cancel(message, config, request) {
// 用token.reason判断是否取消
if (token.reason) {
// 说明已经取消过了
return;
}
// 新建一个异常,赋值给reason
token.reason = new CanceledError(message, config, request);
// 执行后会改变promise的状态为fulfilled,
// 这之后会执行this.promise.then()
// 注意resolve传入的是CanceledError
resolvePromise(token.reason);
});
}
// 如果已经取消过了,抛出异常
throwIfRequested() {
if (this.reason) {
throw this.reason;
}
}
// 添加取消事件订阅
subscribe(listener) {
// 如果已经取消过了,执行listener方法
if (this.reason) {
listener(this.reason);
return;
}
// 否则将该函数加到_listeners数组中
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);
}
}
// 类的静态方法
// 返回token和cancel方法
// 这样就可以通过执行cancel()改变token的状态
static source() {
let cancel;
const token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token,
cancel,
};
}
}
export default CancelToken;
5 请求取消的整体流程
以下代码是我整合了axios请求中多个文件的关键部分代码,有点伪代码的意思,这样看起来比较连贯,好理解。
request (configOrUrl, config) {
// 合并配置
config = mergeConfig(this.defaults, config);
// 合并请求头信息
config.headers = AxiosHeaders.concat(contextHeaders, headers);
// 请求拦截器链
const requestInterceptorChain = [];
this.interceptors.request.forEach(function unshiftRequestInterceptors (interceptor) {
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 响应拦截器链
const responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors (interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
// 执行请求拦截器方法
doRequestInterceptorChain()
// 判断config.cancelToken.reason,也就是是否被取消
throwIfCancellationRequested(config);
// 请求体
let requestData = config.data;
// 创建一个xhr实例
let request = new XMLHttpRequest();
// open xhr
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// 请求状态变更处理函数
request.onreadystatechange = function handleLoad () {
//.....
};
// 订阅取消事件
if (config.cancelToken || config.signal) {
// 当token取消了,会执行该函数,也就是abort
onCanceled = cancel => {
if (!request) {
return;
}
reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
request.abort();
request = null;
};
// 此处是用cancelToken的订阅取消事件
config.cancelToken && config.cancelToken.subscribe(onCanceled);
// 此处是用signal,也就是AbortConrtroller订阅取消事件
if (config.signal) {
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
// 真正发送请求
request.send(requestData || null);
// 再次判断config.cancelToken.reason,也就是是否被取消
throwIfCancellationRequested(config);
// 执行返回拦截器
doResponseInterceptorChain()
}
6 AbrotController
现在再回头,让我们看看官网提供的AbortController
和CancelToken
形式的使用方式:
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// 取消请求
controller.abort()
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 {
// 处理错误
}
});
// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
对比一下两者:
以controller
指代AbortController
以source
指代CancelToken
AbortController | CancelToken | |
---|---|---|
传入方式 | controller.signal | source.token |
判断是否取消 | controller.signal.aborted | source.token.resaon |
订阅取消事件 | controller.signal.addEventListener('abort', onCanceled) | source.token.subscribe(onCanceled) |
用户取消 | controller.abort() | source.cancel() |
对比后我们发现,这两者不能说是很像,只能说是一模一样好吧!
我的理解是:CancelToken
其实就是作者自己实现的一版AbortController
,虽然细节不太一样,但是思路其实是一样的。
7 异常类
最后,再补充介绍一下axios中的异常类:AxiosError
、CanceledError
。
通过名称可以猜到,AxiosError
是作者自定义专门给axios
项目使用的异常类,而CanceledError
是AxiosError
里更具体的一种异常,等于是AxiosError的子类。
7.1 AxiosError.js
// AxiosError的构造函数
function AxiosError(message, code, config, request, response) {
Error.call(this);
// 只有谷歌的V8引擎有该api
if (Error.captureStackTrace) {
// 将捕获的异常堆栈信息写入到this的.stack属性中
Error.captureStackTrace(this, this.constructor);
} else {
// 其他浏览器的写法
this.stack = new Error().stack;
}
this.message = message;
this.name = 'AxiosError';
code && (this.code = code);
config && (this.config = config);
request && (this.request = request);
response && (this.response = response);
// 因为该函数是作为构造函数使用,
// 所以最终会默认返回this,也就是AxiosError
}
// 让AxiosError继承Error的原型
// 额外自定义了一个toJSON方法
utils.inherits(AxiosError, Error, {
toJSON: function toJSON() {
return {
// Standard
message: this.message,
name: this.name,
// Microsoft
description: this.description,
number: this.number,
// Mozilla
fileName: this.fileName,
lineNumber: this.lineNumber,
columnNumber: this.columnNumber,
stack: this.stack,
// Axios
config: utils.toJSONObject(this.config),
code: this.code,
status:
this.response && this.response.status ? this.response.status : null,
};
},
});
const prototype = AxiosError.prototype;
const descriptors = {};
// 自定义了一些异常类型
[
'ERR_BAD_OPTION_VALUE',
'ERR_BAD_OPTION',
'ECONNABORTED',
'ETIMEDOUT',
'ERR_NETWORK',
'ERR_FR_TOO_MANY_REDIRECTS',
'ERR_DEPRECATED',
'ERR_BAD_RESPONSE',
'ERR_BAD_REQUEST',
'ERR_CANCELED',
'ERR_NOT_SUPPORT',
'ERR_INVALID_URL',
// eslint-disable-next-line func-names
].forEach((code) => {
descriptors[code] = { value: code };
});
// 给AxiosError类增加属性
// 比如AxiosError['ERR_BAD_OPTION_VALUE']='ERR_BAD_OPTION_VALUE'
Object.defineProperties(AxiosError, descriptors);
// 给AxiosError原型增加属性
Object.defineProperty(prototype, 'isAxiosError', { value: true });
// 根据error和code创建一个AxiosError异常
// 等于是将一个普通异常转为AxiosError异常
AxiosError.from = (error, code, config, request, response, customProps) => {
// 根据AxiosError原型创建一个异常对象
const axiosError = Object.create(prototype);
// 将error原型上的方法赋给axiosError
utils.toFlatObject(
error,
axiosError,
// 当取到Error时,停止
function filter(obj) {
return obj !== Error.prototype;
},
// 不复制等于isAxiosError的属性
(prop) => {
return prop !== 'isAxiosError';
}
);
// 调用AxiosError的构造函数
// this指向axiosError
AxiosError.call(axiosError, error.message, code, config, request, response);
// 设置原因为原异常
axiosError.cause = error;
// 使用原异常的名称
axiosError.name = error.name;
// 给异常传递一些自定义的属性
customProps && Object.assign(axiosError, customProps);
return axiosError;
};
export default AxiosError;
下面看看inherits
函数是如何实现的:
// 作用:让A继承B
const inherits = (constructor, superConstructor, props, descriptors) => {
// A的原型等于B原型
// AxiosError.prototype=Object.create(Error.prototype)
constructor.prototype = Object.create(
superConstructor.prototype,
descriptors
);
//A的构造函数等于它本身
constructor.prototype.constructor = constructor;
// 设置A的super为B的原型
Object.defineProperty(constructor, 'super', {
value: superConstructor.prototype,
});
// 给A增加自定义属性
props && Object.assign(constructor.prototype, props);
};
上述代码写完,就可以通过new AxiosError('自定义错误')
来创建一个AxiosError
的异常了。
7.2 CanceledError.js
下面来看看CanceledError是怎么写的:
// CanceledError的构造函数
// 内部其实调用的是AxiosError的构造函数
// 只不过指定了code为AxiosError.ERR_CANCELED
// 并且重新指定了名称为CanceledError
function CanceledError(message, config, request) {
AxiosError.call(
this,
message == null ? 'canceled' : message,
AxiosError.ERR_CANCELED,
config,
request
);
this.name = 'CanceledError';
}
// 让CanceledError继承AxiosError的所有原型属性
// CanceledError原型扩展属性{__CANCEL__: true}
utils.inherits(CanceledError, AxiosError, {
__CANCEL__: true,
});
export default CanceledError;
8 结束
今天关于axios
如何实现请求取消的内容就到这里了,如果有疑问,欢迎大家评论区沟通交流!