Axios 请求取消机制详解:AbortController 与 CancelToken 的实现原理及实战
【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios
Axios 从 v0.22.0 起同时支持基于AbortController的新一代取消方式和已废弃的CancelTokenAPI。本文以 Axios 官方取消请求文档为主体,完整覆盖两种 API 的用法、取消错误的判定方式与 TypeScript 类型支持,并深入 lib/cancel 目录下的源码,解析取消信号在调度层与 HTTP/XHR 适配器中的真实流转路径,帮助你在浏览器和 Node.js 中正确、可靠地中断在途请求并妥善处理取消引发的错误。
一、AbortController:推荐的取消方式
从 v0.22.0 起,Axios 支持使用 Web 标准的AbortController以简洁的方式取消请求。该功能在浏览器和 Node.js(使用支持AbortController的版本)中均可用。用法是创建一个AbortController实例,并把它的signal传入请求配置的signal选项:
const controller = new AbortController(); axios .get('/foo/bar', { signal: controller.signal, }) .then(function (response) { //... }); // 取消请求 controller.abort();这是与平台标准fetch的signal参数一致的语义,因此可以直接复用浏览器原生信号、AbortSignal.timeout()或 React/Vue 组件卸载时创建的控制器,无需引入 Axios 私有 API。
源码中的取消检查点
signal并不是直接透传给底层请求对象的孤立选项,而是贯穿了请求生命周期的多个检查点。在调度层 lib/core/dispatchRequest.js 中,throwIfCancellationRequested函数同时检查两种取消来源:
function throwIfCancellationRequested(config) { if (config.cancelToken) { config.cancelToken.throwIfRequested(); } if (config.signal && config.signal.aborted) { throw new CanceledError(null, config); } }它被调用在三个关键时机:
- 请求发出前(dispatchRequest.js 第 41 行):若
signal在请求开始前就已处于 aborted 状态,直接抛出CanceledError,底层适配器根本不会被执行——这正是文档中"已取消状态下发起的请求会立即取消、不会尝试实际网络请求"的实现来源; - 适配器成功返回后(dispatchRequest.js):在
transformResponse处理响应数据前再检查一次,防止慢速响应的数据被继续加工; - 适配器失败路径中(dispatchRequest.js):若失败原因不是取消错误,仍补一次取消检查,把"响应错误"重新归类为取消。
在 Node.js 的 HTTP 适配器 lib/adapters/http.js 中,信号被真正挂接到原生请求上:
if (config.cancelToken || config.signal) { config.cancelToken && config.cancelToken.subscribe(abort); if (config.signal) { config.signal.aborted ? abort() : config.signal.addEventListener('abort', abort); } }可见 Axios 对两种取消机制做了统一归并:cancelToken通过订阅(subscribe)接入,signal通过标准的abort事件接入,且都先判断是否已处于取消状态。请求结束时两者都会被解绑(unsubscribe/removeEventListener,见 http.js 与浏览器适配器的 xhr.js),避免事件监听器泄漏。
浏览器 XHR 适配器同样处理这两种信号,在 lib/adapters/xhr.js 中订阅取消并在done()中清理监听。
二、CancelToken:已废弃但仍可用的经典 API
Axios 也保留了CancelTokenAPI 来取消请求。该 API已废弃,将在下一个主版本中移除,官方建议改用AbortController;但对存量代码而言,它仍是当前版本中功能完整的方案。
方式一:CancelToken.source() 工厂方法
最直观的用法是CancelToken.source()工厂方法,它返回{ token, cancel }对象——把token放入请求配置,把cancel函数留给业务代码:
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 { // 处理错误 } }); axios.post( '/user/12345', { name: 'new name', }, { cancelToken: source.token, } ); // 取消请求(message 参数可选) source.cancel('Operation canceled by the user.');对应源码在 lib/cancel/CancelToken.js 的静态方法source():它通过执行函数捕获内部的cancel函数并暴露出来,返回token与cancel的配对。
方式二:执行函数(executor)模式
也可以向CancelToken构造函数传入执行函数来创建令牌,适合"取消时机由复杂逻辑决定"的场景:
const CancelToken = axios.CancelToken; let cancel; axios.get('/user/12345', { cancelToken: new CancelToken(function executor(c) { // 执行函数接收一个 cancel 函数作为参数 cancel = c; }), }); // 取消请求 cancel();构造函数对参数做了严格校验:executor必须是函数,否则抛出TypeError('executor must be a function.')(CancelToken.js)。此外注意构造函数内部是同步调用executor,cancel必须在构造完成后(通常是请求发出后的回调里)才被赋值使用,不能在构造期间直接调用。
底层辅助方法:subscribe / unsubscribe / toAbortSignal
CancelToken还为旧版集成暴露了一些底层辅助方法,用于把取消令牌桥接到其他监听体系或标准AbortSignal:
const source = axios.CancelToken.source(); const listener = (cancel) => { console.log(cancel.message); }; source.token.subscribe(listener); const signal = source.token.toAbortSignal(); // 将 `signal` 传给接受 AbortSignal 的 API。 source.cancel('Operation canceled by the user.'); source.token.unsubscribe(listener);从源码看这三个方法的实现要点(CancelToken.js):
subscribe(listener):若令牌已被取消(this.reason存在),则同步立即回调listener(this.reason);否则把监听器压入_listeners数组,等待取消发生时统一触发(触发后_listeners置为null,防止重复派发);unsubscribe(listener):从监听数组中移除指定函数;toAbortSignal():内部新建一个AbortController,通过subscribe把取消原因桥接为controller.abort(err),并且给返回的signal挂上了自定义的unsubscribe方法(() => this.unsubscribe(abort)),供如 Axios 适配器composeSignals一类的组合逻辑做清理。
三、取消错误:CanceledError 与 isCancel
被取消的请求会以axios.CanceledError拒绝(reject)。旧版导出axios.Cancel是axios.CanceledError的别名(兼容旧代码),取消错误上还带有__CANCEL__标记,供axios.isCancel判断使用。
这些结论均可在源码中逐一验证:
- lib/cancel/CanceledError.js:
CanceledError继承自AxiosError,构造函数写入错误码AxiosError.ERR_CANCELED、name = 'CanceledError',以及关键标记this.__CANCEL__ = true;当 message 为空时默认使用字符串'canceled'; - lib/cancel/isCancel.js:
isCancel(value)的实现就是!!(value && value.__CANCEL__),即基于__CANCEL__属性做鸭子类型判断,这也是为什么toAbortSignal桥接等路径必须确保最终 reject 的是带该标记的错误对象; - lib/axios.js:
axios.CanceledError、axios.CancelToken、axios.isCancel三个成员的挂载位置,以及第 63 行axios.Cancel = axios.CanceledError的别名声明。
单元测试 tests/unit/cancel/canceledError.test.js 也印证了行为细节:默认消息为CanceledError: canceled,自定义消息原样保留;且node:util/types的isNativeError能识别它为原生错误,方便在通用错误处理管道中与普通Error一致对待。
TypeScript 下的 isCancel 类型收窄
在 TypeScript 中,isCancel<T, D, P>()在收窄unknown错误类型时,会保留响应数据、请求数据和查询参数的泛型:
interface SearchResponse { results: string[]; } interface RequestBody { includeArchived: boolean; } interface SearchParams { query: string; } try { await axios.get("/search"); } catch (error) { if (axios.isCancel<SearchResponse, RequestBody, SearchParams>(error)) { error.response?.data; // SearchResponse | undefined error.config?.data; // RequestBody | undefined error.config?.params; // SearchParams | undefined } }这一能力的根源是CanceledError构造时携带了config与request参数(见 CanceledError.js),而AxiosError基类又把二者挂到了错误实例的config/request上,因此取消错误对象本身就携带了请求上下文,类型系统得以在此基础上做精确收窄。
四、一个令牌取消多个请求与"预取消"行为
你可以使用同一个取消令牌或AbortController取消多个请求。如果在 Axios 请求开始时取消令牌已处于已取消状态,则请求会立即被取消,不会尝试发起实际的网络请求。
这一行为的底层依据在前文已说明:
- 对
AbortController路径,throwIfCancellationRequested在请求发出前检查config.signal.aborted,直接抛出CanceledError(dispatchRequest.js); - 对
CancelToken路径,token.throwIfRequested()在已取消(存在reason)时立即抛出该CanceledError(CancelToken.js); - 适配层对"已取消"的信号同样做了短路处理:
signal.aborted ? abort() : addEventListener(...)(http.js),subscribe对已取消令牌也立即同步回调监听器(CancelToken.js)。
因此批量取消的典型模式是:为一批并发请求共享一个source/controller,需要整体中断时调用一次cancel(message)/abort(),所有在途与未发出的请求都会收到带统一 message 的CanceledError;已经发出且已完成的请求不受影响。
取消与超时的归并处理
值得补充的是,在 Node.js 适配器的流式场景下,取消还会与超时错误发生交互。从 lib/adapters/http.js 的结构看,abortEmitter用once('abort')统一驱动 reject;流被中途断开时以CanceledError('Request stream has been aborted', config, req)拒绝(http.js)。浏览器端取消相关的行为另有专项测试覆盖,可参见 tests/browser/cancel.browser.test.js 与 tests/browser/cancelToken.browser.test.js,单元测试侧则包括 tests/unit/cancel/isCancel.test.js。
五、两种 API 的选择建议
| 维度 | AbortController | CancelToken |
|---|---|---|
| 状态 | 推荐(Web/Node 标准) | 已废弃,将在下一个主版本移除 |
| 配置项 | signal: controller.signal | cancelToken: source.token |
| 触发取消 | controller.abort(reason?) | source.cancel(message?) |
| 与平台 API 互通 | 可直接复用原生AbortSignal | 需经token.toAbortSignal()桥接 |
| 多请求共享 | 共享同一controller | 共享同一source.token |
| 判定取消错误 | axios.isCancel(error) | axios.isCancel(error)(相同) |
迁移路径很直接:把CancelToken.source()换成new AbortController(),把cancelToken: source.token换成signal: controller.signal,把source.cancel(msg)换成controller.abort(msg)——错误判定代码(axios.isCancel/axios.CanceledError)无需改动,因为两条路径最终都归并到同一个CanceledError与__CANCEL__标记上。
【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考