前言
按官网的说法,从 v0.22.0
开始,不建议在新项目里使用 CancelToken
的方式来取消请求,而是使用 AbortController,下面就让我们看看两者的运行原理,以及其他相关问题在源码层面的解析(源码基于1.6.8版本)。
CancelToken 和 AbortController
1. CancelToken
为了更好的理通思路,这里需要引入 3段代码 + 1张关系图:
代码1(官网例子)
js
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 {
// handle error
}
});
// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
代码2 node_modules/axios/lib/cancel/cancelToken.js
js
subscribe(listener) {
if (this.reason) {
listener(this.reason);
return;
}
if (this._listeners) {
this._listeners.push(listener);
} else {
this._listeners = [listener];
}
}
let resolvePromise;
const token = this;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
executor(function cancel(message, config, request) {
// 判断是否已经取消过了
if (token.reason) {
// Cancellation has already been requested
return;
}
// 给token.reason赋值一个异常
token.reason = new CanceledError(message, config, request);
// 传递异常,并将Promise状态改为fulfilled
resolvePromise(token.reason);
});
* Returns an object that contains a new `CancelToken` and a function that, when called,
* cancels the `CancelToken`.
*/
static source() {
let cancel;
const token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token,
cancel
};
}
代码3 node_modules/axios/lib/adapters/xhr.js
js
let request = new XMLHttpRequest();
if (config.cancelToken || config.signal) {
// Handle cancellation
// eslint-disable-next-line func-names
onCanceled = cancel => {
if (!request) {
return;
}
reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
request.abort();
request = null;
};
config.cancelToken && config.cancelToken.subscribe(onCanceled);
if (config.signal) {
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
关系图:

逻辑整理:
-
代码3中,
if (config.cancelToken || config.signal)
说明如果配置了取消请求的参数(目前对两种方式都支持),则会在cancelToken
上通过subscribe
注册一个onCanceled(cancel)
事件,并存到_listeners
中,而onCanceled
中的request.abort
就是用来取消请求的。 -
从关系图我们可以发现,官方例子里
source.cancel
其实就等于右侧圈起来的函数,而这个函数一旦执行,resolvePromise
就会变成fulfilled
并且携带一个CanceledError
参数,然后触发this.promise.then
中的token._listeners[i](cancel)
即上述的onCanceled(cancel)
,cancel
就是通过promise
链传递过来的CanceledError
。
2. AbortController
介绍这个取消方式前,让我们先看下MDN上对 AbortController
的解释:
AbortController
接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。 你可以使用AbortController.AbortController()
构造函数创建一个新的AbortController
。使用
AbortSignal
对象可以完成与 DOM 请求的通信。
翻译一下,就是可以用来终止请求,通过 new AbortController()
创建一个实例对象,该对象下面有signal
属性和abort
方法,
signal
:只读,返回一个AbortSignal
对象实例。abort
:中止一个尚未完成的 Web请求,该函数还可以传入一个参数,用来自定义终止请求的原因,这个参数最终会被signal.reason
(下面会介绍)接收。
MDN引用了fetch
作为例子,将signal
传入fetch
(fetch配置有个signal
选项),内部会监测这个对象的状态,如果这个对象的状态从未终止的状态变为终止的状态的话,并且 fetch
请求还在进行中的话,fetch
请求就会立即失败。其对应的 Promise
的状态就会变为 Rejected
。
那么要如何更改signal
的状态呢?只要调用abort
方法就可以。
js
let controller;
const url = "video.mp4";
const downloadBtn = document.querySelector(".download");
const abortBtn = document.querySelector(".abort");
downloadBtn.addEventListener("click", fetchVideo);
abortBtn.addEventListener("click", () => {
if (controller) {
controller.abort();
console.log("中止下载");
}
});
function fetchVideo() {
controller = new AbortController();
const signal = controller.signal;
fetch(url, { signal })
.then((response) => {
console.log("下载完成", response);
})
.catch((err) => {
console.error(`下载错误:${err.message}`);
});
}
AbortSignal实例 又是什么呢?
上面提到signal
其实是一个AbortSignal
对象实例(下面描述用AbortSignal
替代signal
),而该实例对象有两个属性和两个方法(还有其他的属性方法,这里暂时只需要这4个):
-
aborted :只读,一个
Boolean
值,表示与之通信的请求是否被终止(true
)或未终止。(false
) -
reason :只读,可以是任何的
JavaScript
类型的值,上面提到的new AbortController().abort
传过来的 自定义终止请求的原因 就在这里接收。 -
abort :返回一个已经被设置为中止的
AbortSignal
实例(AbortSignal.aborted===true
),在这个方法里也可以传 自定义终止请求的原因 ,和new AbortController().abort
等效。 -
timeout :返回一个在指定事件后将自动终止的
AbortSignal
实例,即设置一个时间段,当这个时间段过去后会自动触发AbortSignal
的abort
方法,从而中止一个操作或请求
回到Axios
源码 node_modules/axios/lib/adapters/xhr.js
:

结合我们前面学习的AbortController
,如果aborted
为true
,表示请求被中止,所以调用取消请求的函数onCanceled
;否则,将onCanceled
绑定到给AbortSignal
的abort
函数,只要调用AbortSignal.abort
就会触发。
tips:
1.一旦
AbortSignal
被终止(即调用了abort
方法),它就不能再被用于中止其他操作。终止后的AbortSignal
对象将保持在终止状态,无法再次用于中止其他操作或请求。如果需要的话,可以创建新的AbortSignal
对象来控制。2.同一个
AbortSignal
对象可以同时传递给多个请求,在需要的情况下可以同时取消多个请求
拓展
AbortController
可以配合 addEventListener使用,以前如果需要removeEventListenr(event, callback)
,callback
必须和addEventListener
上绑定的是同一个callback
,这样写起来其实比较麻烦,而现在可以改成用AbortSignal
来控制。

举个🌰:
js
const controller = new AbortController();
function callback (e) {
document.addEventListener('mousemove', (e) => {
// ...
},{
signal: controller.signal
});
}
document.addEventListener('mousedown', callback);
document.addEventListener('mouseup', controller.abort);
- 用于普通函数:
js
function fetchData(url, signal) {
return new Promise((resolve, reject) => {
fetch(url, { signal })
.then(response => {
resolve(response);
})
.catch(error => {
if (error.name === 'AbortError') {
reject(new Error('请求已被中止'));
} else {
reject(error);
}
});
});
}
const controller = new AbortController();
const signal = controller.signal;
// 发起数据请求
fetchData('https://api.example.com/data', signal)
.then(data => {
console.log('成功获取数据:', data);
})
.catch(error => {
console.error('数据请求失败:', error);
});
// 0.3秒后中止请求
setTimeout(() => {
controller.abort();
}, 300);
- 个人理解,
AbortSignal
就像一棵树,各种动物都可以往树上爬(绑定事件),但是有一天如果树倒了(被中止aborted
),所有的动物也就都掉了下来(取消进程),而且这棵树不会再立起来(终止后的AbortSignal
无法再次用于中止其他操作或请求),动物们也要去寻找新的树了(创建新的AbortSignal
对象)。
axios和instance的区别
instance
是通过axios.create
创建出来的,但是它和axios
一样,能发送任意请求,也有默认的配置和拦截器等,那它们到底有什么区别呢? 沿着axios
的引用依赖摸索,我们在node_modules/axios/lib/axios.js
找到了相关代码。
js
/**
* Create an instance of Axios
*
* @param {Object} defaultConfig The default config for the instance
*
* @returns {Axios} A new instance of Axios
*/
function createInstance(defaultConfig) {
const context = new Axios(defaultConfig);
const instance = bind(Axios.prototype.request, context);
// Copy axios.prototype to instance
utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
// Copy context to instance
utils.extend(instance, context, null, {allOwnKeys: true});
// Factory for creating new instances
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
return instance;
}
// Create the default instance to be exported
const axios = createInstance(defaults);
// Expose Axios class to allow class inheritance
axios.Axios = Axios;
// Expose Cancel & CancelToken
axios.CanceledError = CanceledError;
axios.CancelToken = CancelToken;
axios.isCancel = isCancel;
axios.VERSION = VERSION;
axios.toFormData = toFormData;
// Expose AxiosError class
axios.AxiosError = AxiosError;
// alias for CanceledError for backward compatibility
axios.Cancel = axios.CanceledError;
// Expose all/spread
axios.all = function all(promises) {
return Promise.all(promises);
};
其中 createInstance
就是我们苦苦寻找的核心点,而且由const axios = createInstance(defaults)
也可得知,aixos
是通过它创建的。这个函数的基本功能如下:
- 创建
Axios
实例:context
- 通过
bind
将request
的this
指向context
,并生成了一个新函数instance
,bind
方法其实是基于apply
封装起来的,详见源码如下:
js
export default function bind(fn, thisArg) {
return function wrap() {
return fn.apply(thisArg, arguments);
};
}
- 通过工具包
utils
,将Axios.prototype
和实例对象的方法都绑定到instance
函数的身上 - 然后给
instance
绑定create
方法(就如注释所说,就是个创建实例的工厂函数),这个create
就是axios.create
的源码,它其实返回的也是通过createInstance
创建的函数。
看到这里,会发现axios
和instance
都是由一个共同函数创建的(都基于最原始的Axios
),难道真的一模一样!?
其实往下看就会发现,还是有差别的:
在
createInstance
函数下面,给axios
还添加了CanceledError
、CancelToken
、toFormData
等方法,这些都是通过axios.create
创建出来的instance
函数所不具有的。
拦截器的执行顺序
如果同时定义多个拦截器,那么他们的执行顺序又是如何呢,上代码:
js
axios.interceptors.request.use(function (config) {
console.log('request---1')
return config;
});
axios.interceptors.request.use(function (config) {
console.log('request---2')
return config;
});
axios.interceptors.response.use(function (config) {
console.log('response---1')
return config;
});
axios.interceptors.response.use(function (config) {
console.log('response---2')
return config;
});
axios.get('https://www.thecocktaildb.com/api/json/v1/1/search.php?s=margarita')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

然后会发现请求拦截器的顺序和代码顺序是反的,而响应拦截器是正序,老样子,找源码去:

从中我们可以发现,请求和响应拦截器其实用的一个类 InterceptorManager
,然后在该类中,通过use
方法,将定义的拦截器都存到hanlders
里。
关键点来了 ,最右边代码拿拦截器实例的时候(foreach
是InterceptorManager
类里的一个方法,用于从hanlders
里遍历数据),requestInterceptorChain
是用的shift
,而responseInterceptorChain
用的push
,而两种拦截器添加的时候都是用的push
,所以依旧用push
的响应拦截器顺序不变,而用shift
的请求拦截器则发生了倒序。
tips:两个数组方法.apply相当于ES6的拓展运算符
其他小问题
- 如何判断为绝对URL :找到了这个判断方法,但是好像并没有被引用。然后发现
node_modules/axios/lib/core/Axios.js
里的buildFullPath
方法可以构建出绝对路径,可能因此弃用了?...

- 配置的优先级 :用户配置和默认配置的优先度是通过
node_modules/axios/lib/core/Axios.js
里的mergeConfig
函数来处理的。具体来说,用户配置会覆盖默认配置的相同属性,但不会完全替换默认配置。这意味着用户可以只提供他们想要覆盖的配置,而不必提供所有配置。以下是mergeConfig
函数的简化版本:
js
function mergeConfig(defaultConfig, userConfig) {
const config = { ...defaultConfig };
// Merge user config into default config
for (const key in userConfig) {
if (userConfig[key] !== undefined) {
config[key] = userConfig[key];
}
}
return config;
}
- 各种方法是怎么挂载的:

- params是如何自动拼接到地址栏的:

Axios运行流程

欢迎小伙伴留言讨论,互相学习!
❤❤❤ 如果对你有帮助,记得点赞收藏哦!❤❤❤