Axios 请求取消机制详解:AbortController、CancelToken 兼容 API 与底层取消链路
【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios
Axios 从 v0.22.0 起通过AbortController提供了一套跨浏览器与 Node.js 的标准请求取消方案,同时保留了处于弃用状态的CancelToken旧 API 用于兼容存量代码。本文基于仓库文档 cancellation.md,逐条覆盖两种取消方式的用法、CanceledError错误识别与 TypeScript 类型收窄,并结合 lib/cancel 与 lib/adapters 下的真实源码,拆解信号从配置项一路传递到 XHR/fetch/http 适配器的完整取消链路,帮助你在实际项目中安全地接入并排查请求取消问题。
为什么取消请求是 HTTP 客户端的刚需
在搜索联想、轮询接口、组件卸载(如 React 的useEffectcleanup)等场景中,往往需要在响应返回之前主动放弃一次尚未完成的请求,避免过期响应污染 UI 或浪费带宽。Axios 的取消机制解决的就是这个问题:调用方只需把取消"信号"交给请求配置,一旦触发取消,进行中的请求会被尽快终止,并最终以一个可识别的取消错误(CanceledError)拒绝 Promise,而不是表现为普通网络错误或超时。
Axios 目前提供两代 API:
| 能力 | AbortController | CancelToken(Deprecated) |
|---|---|---|
| 推荐状态 | 官方推荐(v0.22.0 起) | 已弃用,将在下一个大版本移除 |
| 配置项 | signal | cancelToken |
| 取消触发 | controller.abort() | source.cancel(message)或 executor 捕获的cancel() |
| 跨多个请求复用 | 支持 | 支持 |
| 平台要求 | 运行环境需支持 AbortController(现代浏览器与较新的 Node.js) | 由 axios 自身实现,兼容更老环境 |
方式一(推荐):AbortController + signal
用法非常直接:创建AbortController实例,把它的signal传给请求的signal配置项,需要取消时调用controller.abort():
const controller = new AbortController(); axios .get('/foo/bar', { signal: controller.signal, }) .then(function (response) { //... }); // cancel the request controller.abort();该 API 的优势在于它不是 axios 私有的,而是 Web 标准(fetch、WebSocket 等同样使用),取消语义在所有现代浏览器和支持 AbortController 的 Node.js 版本中一致。
底层原理:信号如何进入适配器。请求进入调度阶段后,axios 会把signal与各适配器自身的终止能力绑定:
- XHR 适配器(浏览器):在 lib/adapters/xhr.js 中,若存在
config.signal,会先检查signal.aborted——已处于 aborted 状态则立即中止,否则监听abort事件调用xhr.abort();请求完成后会解绑监听(removeEventListener('abort', onCanceled)),防止泄漏。 - http 适配器(Node.js):lib/adapters/http.js 对
config.signal做同样的处理,触发时中止底层 Node 请求句柄,并在请求结束时移除abort监听。 - fetch 适配器:lib/adapters/fetch.js 通过
composeSignals把signal与cancelToken(如有)合成一个信号,再挂到 fetch 的signal选项上。
也就是说,signal并不只是被存储起来,而是被真正接到了底层传输的中止开关上。
方式二(已弃用):CancelToken
CancelTokenAPI 已弃用并将在下一个大版本移除,官方建议迁移到AbortController。但存量代码中大量存在该用法,理解它依然必要。
1. 通过 CancelToken.source 工厂创建
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 } }); axios.post( '/user/12345', { name: 'new name', }, { cancelToken: source.token, } ); // cancel the request (the message parameter is optional) source.cancel('Operation canceled by the user.');对应源码见 lib/cancel/CancelToken.js:source()内部实际就是构造一个CancelToken,把 executor 里收到的cancel函数闭包出来返回。source.cancel(message)调用时会向 token 写入一个CanceledError(消息缺省为'canceled'),并 resolve 内部 Promise 触发所有监听者。
2. 通过 executor 函数创建
const CancelToken = axios.CancelToken; let cancel; axios.get('/user/12345', { cancelToken: new CancelToken(function executor(c) { // An executor function receives a cancel function as a parameter cancel = c; }), }); // cancel the request cancel();从 CancelToken 构造函数 可以看到几个关键实现细节:
- executor 必须传入函数,否则抛出
TypeError('executor must be a function.'); - 构造时创建一个内部 Promise,取消发生时
token.reason被赋值为new CanceledError(message, config, request),随后 resolve 该 Promise,所有subscribe过的监听者依次收到取消原因; - 重复取消是幂等的:若
token.reason已存在,后续的 cancel 调用直接返回,不会二次触发。
3. 低级辅助方法:subscribe / unsubscribe / toAbortSignal
CancelToken还为遗留集成暴露了低级辅助能力:
const source = axios.CancelToken.source(); const listener = (cancel) => { console.log(cancel.message); }; source.token.subscribe(listener); const signal = source.token.toAbortSignal(); // Pass `signal` to APIs that accept AbortSignal. source.cancel('Operation canceled by the user.'); source.token.unsubscribe(listener);其中toAbortSignal()值得注意:见 CancelToken.js 的实现,它内部new AbortController(),把取消原因通过subscribe(abort)桥接为controller.abort(err),并额外给返回的signal挂了一个unsubscribe方法用于反向解绑。这正是 axios 内部能同时接受cancelToken与signal两种配置、并在 fetch 适配器里用composeSignals合成统一信号的基础——也就是说,弃用 API 与标准 API 在底层是互通的。
subscribe(listener)有一个便利行为:如果订阅时取消已经发生,监听者会同步立即收到取消原因,而不会错过事件(见 CancelToken.js)。
识别取消:CanceledError 与 axios.isCancel
被取消的请求会以axios.CanceledError拒绝。查看 lib/cancel/CanceledError.js:
CanceledError继承自AxiosError,错误码为AxiosError.ERR_CANCELED,默认消息为'canceled';- 实例上带有
name = 'CanceledError'与__CANCEL__ = true两个标记。
其中__CANCEL__是历史兼容位:旧版 axios 用axios.isCancel判断取消,其实现(lib/cancel/isCancel.js)只检查value.__CANCEL__这一标志位:
export default function isCancel(value) { return !!(value && value.__CANCEL__); }因此旧代码axios.isCancel(err)对新版抛出的CanceledError依然有效。此外,legacy 的axios.Cancel导出是axios.CanceledError的别名,方便平滑迁移。单元测试 tests/unit/cancel/canceledError.test.js 也验证了默认/自定义消息的toString()输出(CanceledError: canceled/CanceledError: <message>)。
调度层的兜底检查。除适配器内的中止外,axios 在请求调度边界还会做两次主动检查,见 lib/core/dispatchRequest.js 的throwIfCancellationRequested:
- 请求发出前:若
config.cancelToken已请求取消则调用throwIfRequested()直接抛出;若config.signal.aborted为真则立即抛出CanceledError——即"发起时已取消的请求根本不会真正发出去"(后文详述); - 请求完成后(adapter resolve 时)与失败时(非取消原因):再次检查,保证响应处理阶段的竞态也能被识别为取消。
TypeScript:isCancel 的泛型重载
在 TypeScript 项目中,axios.isCancel提供了泛型签名,可以在把unknown类型的 catch 错误收窄为取消错误的同时,保留响应数据、请求体和查询参数的类型:
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 } }三个类型参数分别对应isCancel<T, D, P>()的响应数据类型、请求数据类型与查询参数类型,签名定义可参考仓库根目录的 index.d.ts。这避免了取消分支中大量as断言。
信号合成:一个请求中同时有 timeout、signal 与 cancelToken 时
真实配置里timeout、signal、cancelToken可能同时存在。axios 用 lib/helpers/composeSignals.js 将它们合并为一个内部AbortController的信号:
- 收集所有真值信号(
signals.filter(Boolean)),若既无信号也无 timeout 则直接返回(不创建多余对象); timeout到期时以AxiosError(..., ETIMEDOUT)触发合成信号 abort;- 任一外部信号触发
abort(或CancelToken的unsubscribe风格取消回调)时,立即 abort 合成信号,并把原因包装成CanceledError(若原因本身是AxiosError则原样透传),同时通过unsubscribe()一次性清除所有监听与定时器,保证不泄漏; - 若某个传入的
signal在注册时已经 aborted,会立刻同步触发onabort(composeSignals.js)。
合成后的signal还挂了unsubscribe(utils.asap(unsubscribe)),供适配器在请求结束时调用,及时释放监听。fetch 适配器即通过该机制把signal、cancelToken.toAbortSignal()与超时统一汇入 fetch 的signal选项(见 lib/adapters/fetch.js)。
实用行为:一个 token 取消多个请求 / 已取消的 token 立即拒绝
两个值得记住的行为(均来自 cancellation.md):
- 同一 token/controller 可取消多个请求。
CancelToken.source()返回的token或同一个controller.signal可以同时传给多个 axios 请求,调用一次cancel()/abort()全部作废。这在"翻页时取消所有在途的旧请求"场景中非常实用。 - token 已取消时,新请求立即取消。如果取消 token 在请求发起时刻已被取消,axios 会直接以取消错误拒绝 Promise,不会发起任何真实网络请求。这一点由 dispatchRequest.js 的预检逻辑与 composeSignals.js 对已 aborted 信号的同步处理共同保证,源码层面可验证。
一个基于 AbortController 的"取消在途请求"典型写法:
// 每次搜索前先取消上一次未完成的请求 let controller = null; async function search(query) { controller?.abort(); controller = new AbortController(); try { const { data } = await axios.get('/search', { params: { query }, signal: controller.signal, }); return data; } catch (err) { if (axios.isCancel(err)) { // 被主动取消,静默处理即可 return; } throw err; } }小结
- 新代码一律使用
AbortController+signal配置项;它是 Web 标准,浏览器与 Node.js 通用; CancelToken(source()/ executor /subscribe/toAbortSignal)已弃用但当前仍可用,且通过toAbortSignal()与标准信号互通,存量代码可渐进迁移;- 取消错误统一为
CanceledError(ERR_CANCELED、__CANCEL__),用axios.isCancel判断;TS 下可用isCancel<T, D, P>()收窄类型; - 取消信号在 dispatchRequest.js 的预检/后检、composeSignals.js 的信号合成,以及各适配器(xhr.js、http.js、fetch.js)的事件绑定三层共同生效,"已取消 token 发起请求立即拒绝"的行为有明确的源码依据。
【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考