用了这么久的CancelToken,Axios竟推荐用AbortController取而代之
前言
对于取消请求,Axios官方曾经推出了CancelToken来实现该功能。而在 2021 年 10 月推出的AxiosV0.22.0版本中却把CancelToken打上 👎deprecated 的标记,意味废弃。与此同时,推荐 AbortController 来取而代之,如下所示:

大家也可以点击axios#cancellation来阅读关于图片上的文档出处。
针对这个新的用法,我对此进行学习且总结出这篇文章,这篇文章主要的内容点如下:
AbortController是什么?Axios内部是如何运用AbortController的?- 个人分析:
Axios为什么推荐用AbortController替代CancelToken?
下面就直接开始进入本文的内容吧。
本文所分析的Axios源码版本为v0.27.2
AbortController是什么?
直接引用MDN AbortController来介绍:
AbortController 接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。
你可以使用 AbortController.AbortController() 构造函数创建一个新的 AbortController。使用 AbortSignal 对象可以完成与 DOM 请求的通信。
可能上面的概念会有点抽象,下面我直接通过一个例子来展示如何用AbortController中断用XHR请求,代码如下所示:
import { useRef, useState } from "react";
export default function App() {
const [message, setMessage] = useState("");
const controller = useRef();
const [loading, setLoading] = useState(false);
const requestVideo = () => {
setMessage("下载中");
setLoading(true);
// 创建AbortController实例且存放到controller上
// 注意这里每次请求都会创建一个新的AbortController实例,是因为AbortController实例调用abort后,
// AbortController实例的状态signal就为aborted不能更改
controller.current = new AbortController();
const xhr = new XMLHttpRequest();
xhr.open("get", "https://mdn.github.io/dom-examples/abort-api/sintel.mp4");
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
setMessage("下载成功");
setLoading(false);
}
};
// 监听AbortController实例的abort事件,当AbortController实例调用abort方法时,就会触发该事件执行回调
controller.current.signal.addEventListener("abort", () => {
setMessage("下载中止");
setLoading(false);
xhr.abort();
});
xhr.send();
};
// 调用AbortController实例的abort方法,从而触发上面注册在abort事件的回调的执行
const abortDownload = () => {
controller.current.abort();
};
return (
<>
<div>
<button disabled={loading} onClick={requestVideo}>
Download video
</button>
<button disabled={!loading} onClick={abortDownload}>
Abort Download
</button>
</div>
<div>{message}</div>
</>
);
}
交互效果如下所示:

大家也可以在这里CodeSandbox体验上面的代码例子。体验的时候最好把网络环境设为“Slow 3G”,这样子接口响应时间长一点,能及时禁止请求。

从上面的例子中可知,AbortController的实例abortController只是一个类似观察者模式(如上图所示 👆)中的Subject(即事件派发中心),通过abortController.signal.addEventListener('abort', callback)注册Observer。且负责中断 Web 请求的是这些被注册在Subject上的Observer。且这个Subject是一次性的,即只能notify一次。当abortController.abort被调用时,作为信号状态的abortController.signal的aborted属性(只读值)置为true,表示该信号状态已被取消。
AbortController常用于取消Fetch请求,其取消Fetch请求的代码逻辑非常简洁明了,如下代码所示:
import { useRef, useState } from "react";
export default function App() {
const [message, setMessage] = useState("");
const controller = useRef(new AbortController());
const [loading, setLoading] = useState(false);
const fetchVideo = () => {
setMessage("下载中");
setLoading(true);
controller.current = new AbortController();
fetch("https://mdn.github.io/dom-examples/abort-api/sintel.mp4", {
// fetch配置中仅需把signal指向AbortController实例的signal即可
signal: controller.current.signal,
})
.then(() => {
setMessage("下载成功");
setLoading(false);
})
.catch((e) => {
setMessage("下载错误:" + e.message);
setLoading(false);
});
};
const abortDownload = () => {
controller.current.abort();
};
return (
<>
<div>
<button disabled={loading} onClick={fetchVideo}>
Download video
</button>
<button disabled={!loading} onClick={abortDownload}>
Abort Download
</button>
</div>
<div>{message}</div>
</>
);
}
交互效果和XHR例子的一样,这里就不展示了,想体验的读者可以点击此处Code Sandbox。
对于AbortController的浏览器兼容性如下所示:

可见,如果项目针对的浏览器版本比较旧,那在Axios上还是乖乖用CancelToken来取消请求比较好。
Axios内部是如何运用AbortController的?
我们直接来看看Axios源码中是如何使用AbortController的。首先要知道,在Axios中负责发出请求的是axios.default.adapter,而在浏览器环境下axios.default.adapter取自'lib/adapters/xhr.js'文件,下面来看看这个文件中涉及到XHR和AbortController和CancelToken(为了方便下面分析CancelToken)的源码:
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var onCanceled;
// done函数用于在请求结束后注销回调函数,以免发生内存泄漏
function done() {
// 如果使用CancelToken实例,则会在下面发出请求逻辑之前通过subscribe注册onCanceled函数
if (config.cancelToken) {
config.cancelToken.unsubscribe(onCanceled);
}
// 如果使用AbortController实例,则会在下面发出请求逻辑之前通过signal.addEventListener监听abort事件且注册onCancel作为回调函数
if (config.signal) {
config.signal.removeEventListener("abort", onCanceled);
}
}
var request = new XMLHttpRequest();
var fullPath = buildFullPath(config.baseURL, config.url);
request.open(
config.method.toUpperCase(),
buildURL(fullPath, config.params, config.paramsSerializer),
true
);
function onloadend() {
// 生成response对象
var response = { data, status, statusText, headers, config, request };
// settle函数内部根据response.status或config.validateStatus去调用_resolve或_reject
settle(
function _resolve(value) {
resolve(value);
done();
},
function _reject(err) {
reject(err);
done();
},
response
);
}
if ("onloadend" in request) {
// Use onloadend if available
request.onloadend = onloadend;
} else {
// Listen for ready state to emulate onloadend
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
if (
request.status === 0 &&
!(request.responseURL && request.responseURL.indexOf("file:") === 0)
) {
return;
}
// onreadystatechange事件会先于onerror或ontimeout事件触发
// 因此onloaded需要在下一个事件循环中执行
setTimeout(onloadend);
};
}
// Handle browser request cancellation (as opposed to a manual cancellation)
request.onabort = function handleAbort() {};
// Handle low level network errors
request.onerror = function handleError() {};
// Handle timeout
request.ontimeout = function handleTimeout() {};
// 处理用到CancelToken或AbortController的情况
if (config.cancelToken || config.signal) {
// 取消请求的函数
onCanceled = function (cancel) {
if (!request) {
return;
}
reject(
!cancel || (cancel && cancel.type) ? new CanceledError() : cancel
);
request.abort();
request = null;
};
// 如果是用CancelToken取消请求,则把onCanceled注册到CancelToken实例上,
// CancelToken实例本质上是一个观察者模式中的Subject,有关其源码会在下面的章节中分析
config.cancelToken && config.cancelToken.subscribe(onCanceled);
// 如果是用AbortController,则先从AbortController实例的signal.aborted判断其是否已调用abort,
// 如果已调用,直接执行onCanceled,如果没有则直接在signal上监听其事件,逻辑和开头展示AbortController取消XHR请求的例子一样
// axios.request在调用时,会return一条动态生成的promise链,链上的顺序是:
// Promise.resove(config)->所有请求拦截器(onFulfilled,onRejected)->(dispatchRequest,undefined)->所有响应拦截器(onFulfilled,onRejected)
// dispatchRequest就是调用config.adapter或default.adapter去发出请求,
// 因为存在执行请求拦截器途中,AbortController实例已调用aborted的情况,因此这里要对config.signal.aborted做判断处理
if (config.signal) {
config.signal.aborted
? onCanceled()
: config.signal.addEventListener("abort", onCanceled);
}
}
request.send(requestData);
});
};
根据上面的源码分析可知,AbortContoller的调用方式和上一章节中AbortContoller取消XHR请求的逻辑是一样的,非常浅显易懂。
上面源码中同样也展示了CancelToken实例在其中的运行逻辑:
- 在请求发出之前,
CancelToken实例通过自身方法subscribe注册onCancel函数 - 在请求结束后,
CancelToken实例通过自身方法unsubscribe注销onCancel函数
由此可见,CancelToken实例本质上其实也是一个以观察者模式为原理的事件派发中心。在下面的章节中,我们会顺带学习一下CancelToken的源码。
Axios为什么推荐用AbortController替代CancelToken?
在分析CancelToken被替代之前,我们要先阅读CancelToken源码以学习其内在原理
关于CancelToken的原理
Axios提供了以下方式来运用到CancelToken:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.post(
"/user/12345",
{
name: "new name",
},
{
cancelToken: source.token,
}
);
source.cancel();
我们来按照axios.post的执行过程逐步分析CancelToken对应的源码:
-
首先通过
CancelToken.source方法生成source变量:这里首先要知道生成的
source是什么,我们看下关于CancelToken.source的源码:CancelToken.source = function source() { var cancel; var token = new CancelToken(function executor(c) { cancel = c; }); return { token: token, cancel: cancel, }; };CancelToken.source返回的是token和cancel都取值于CancelToken实例化过程,那我们直接看CancelToken的构造函数:function CancelToken(executor) { if (typeof executor !== "function") { throw new TypeError("executor must be a function."); } var resolvePromise; this.promise = new Promise(function promiseExecutor(resolve) { resolvePromise = resolve; }); var token = this; // eslint-disable-next-line func-names this.promise.then(function (cancel) { if (!token._listeners) return; var i; var l = token._listeners.length; for (i = 0; i < l; i++) { token._listeners[i](cancel); } token._listeners = null; }); // source.cancel指向此处的cancel函数 executor(function cancel(message) { // token.reason有值代表cancel已被执行,CancelToken是一个一次性的Subject,notify一次后即失效 if (token.reason) { // Cancellation has already been requested return; } token.reason = new CanceledError(message); // resolvePromise执行时,会执行上面this.promise.then中传入的回调函数。从而把listeners全执行 resolvePromise(token.reason); }); } -
在
axios.post执行时,调用axios.default.adapter处理发出请求的环节。其中涉及到CancelToken的代码在上一章节分析xhrAdapter源码时已经展示过了,这里就不重复了。下面列出关于CancelToken实例在整个请求过程中的操作:- 发出请求前:通过
config.cancelToken.subscribe(onCanceled)把onCanceled注册到CancelToken实例里。onCanceled内部含request.abort()中断请求操作。 - 在请求完成后:通过
config.cancelToken.unsubscribe(onCanceled)注销该回调函数。
据此,我们来看看
CancelToken中关于subscribe和unsubscribe的源码:CancelToken.prototype.subscribe = function subscribe(listener) { // 如果CancelToken实例已经执行cancel,直接执行该回调函数 if (this.reason) { listener(this.reason); return; } // 如果CancelToken实例还没执行cancel,则把回调函数放进_listeners里 if (this._listeners) { this._listeners.push(listener); } else { this._listeners = [listener]; } }; // 把回调函数从_listeners中移除 CancelToken.prototype.unsubscribe = function unsubscribe(listener) { if (!this._listeners) { return; } var index = this._listeners.indexOf(listener); if (index !== -1) { this._listeners.splice(index, 1); } }; - 发出请求前:通过
至此CancelToken的原理分析完,设计逻辑非常简单,其实也是观察者模式的运用。
个人分析
个人分析Axios官方更推荐使用AbortController的原因如下:
-
保持与
fetch一样的调用方式,让开发者更好上手Axios官方一直保持自身的调用方式与fetch相似,如下所示:fetch(url,config).then().catch() axios(url,config).then().catch()而目前
fetch唯一中断请求的方式就是与AbortController搭配使用。Axios通过支持与fetch一样调用AbortController实现中断请求的方式,让开发者更方便地从fetch切换到Axios。目前就实用性而言,XHR还是比fetch要好,例如sentry在记录面包屑的接口信息方面,XHR请求可以比fetch请求记录更多的数据。还有目前fetch还不支持onprogress这类上传下载进度事件。 -
旧版本(
v0.22.0之前)的CancelToken存在内存泄露隐患,官方想让更多人升级版本从而减少内存泄露风险用
AbortController来中断请求实在v0.22.0版本支持的。而且在v0.22.0之前,CancelToken的运行过程中出现内存泄露隐患。我们来分析一下为什么存在隐患:拿
v0.21.4版本的源码来分析,当时CancelToken不存在CancelToken.prototype.subscribe和CancelToken.prototype.unsubscribe以及内部属性_listeners,且其构造函数如下所示:function CancelToken(executor) { if (typeof executor !== 'function') { throw new TypeError('executor must be a function.'); } var resolvePromise; this.promise = new Promise(function promiseExecutor(resolve) { resolvePromise = resolve; }); /** v0.22.0才新增了这段代码,用_listeners记录回调函数 this.promise.then(function (cancel) { if (!token._listeners) return; var i; var l = token._listeners.length; for (i = 0; i < l; i++) { token._listeners[i](cancel); } token._listeners = null; }); */ var token = this; executor(function cancel(message) { if (token.reason) { // Cancellation has already been requested return; } token.reason = new Cancel(message); resolvePromise(token.reason); }); }在
xhrAdapter中只有下面的代码中涉及到CancelToken:// lib\adapters\xhr.js function xhrAdapter(config) { // .... if (config.cancelToken) { // Handle cancellation config.cancelToken.promise.then(function onCanceled(cancel) { if (!request) { return; } request.abort(); reject(cancel); // Clean up request request = null; }); } // .... }早期的思路是,当
CancelToken实例执行cancel方法时,实例内部属性this.promise状态置为fulfilled,从而执行在xhrAdapter中用then传入的onCanceled函数,从而达到取消请求的目的。在
Axios官方教程对CancelToken的描述中,注明了可以给多个请求注入同一个CancelToken,以达到同时取消多个请求的作用,如下所示:Note: you can cancel several requests with the same cancel token.
const CancelToken = axios.CancelToken; const source = CancelToken.source(); axios.get('/user/1', {cancelToken: source.token}) axios.get('/user/2', {cancelToken: source.token}) // 此操作可同时取消上面两个请求 source.cancel()这种用法使用场景比较多,例如在对大文件时做切片上传的场景,如果需要实现手动中断上传的功能,可以生成一个
CancelToken实例,注入到每一个上传切片的请求上。当用户点击"中断传输"的按钮时,直接执行CancelToken实例的cancel方法即可中断所有请求,代码如下所示:let cancelToken // 上传函数 function upload(){ // 用于存放切片 const chunks = [] // 每个切片的最大容量为5M const SIZE = 5 * 1024 for(let i = 0; i<file.size;i+=SIZE){ chunks.push(file.slice(i,i+size)) } cancelToken = axios.CancelToken.source(); chunks.forEach((chunk)=>{ axios('upload',{ method:'post', cancelToken: cancelToken.token }) }) } // 中断上传函数 function cancel(){ // 执行cancel后会中断上面所有切片的上传 cancelToken.cancel() cancelToken = null }但正是这种玩法存在内存泄露的隐患。假设上面切片上传过程中没有发生中断或者很久才发生中断,则
cancelToken.promise会一直存在在内存里,而由于xhrAdapter中cancelToken.promise通过.then(function onCancel(){...})挂载了很多个onCancel。而我们再来看看onCancel源码:function xhrAdapter(config) { config.cancelToken.promise.then(function onCanceled(cancel) { if (!request) { return; } request.abort(); reject(cancel); // Clean up request request = null; }); }会发现
request并不是onCancel的局部变量,那么说request是通过闭包机制访问到的。当一个请求已经结束时,request因为仍被onCancel引用,所以没在gc过程中从内存堆里被清理。而这些request因为每一个都包含了当前上传数据所以占用相当大,所有这些request会一直存在内存堆里,直至cancelToken执行cancel或者cancelToken置为null值时,cancelToken.promise才会被清除。
如果是在上传单个或者数个非常大的文件,则会非常占用内存从而出现泄露的情况。在
axios的issue里就有两个是涉及到这种情况的:#1181,#3001后来
v0.22.0版本中,Axios官方把CancelToken做了大改,改成了上一节中分析到的CancelToken源码的情况。与此同时,v0.22.0也开始支持AbortController。因此官方开始推荐AbortController,想让开发者升级版本到v0.22.0以上的同时,消除CancelToken带来的内存泄露隐患。 -
减少代码维护量
经历了
v0.22.0的大改后,CancelToken的原理和AbortController相似。既然有AbortController这种在功能上完全顶替CancelToken,且浏览器兼容性好的原生API。就没必要在继续维护CancelToken。估计在之后v1.x或者v2.x版本里不再存在CancelToken,也减少代码的维护量。
后记
这篇文章写到这里就结束了,如果觉得有用可以点赞收藏,如果有疑问可以直接在评论留言,欢迎交流 👏👏👏。
转载自:https://juejin.cn/post/7134326391977279501