完全指南:执行顺序、synchronous 与 runWhen 的源码级解析)
Axios 拦截器Interceptors完全指南执行顺序、synchronous 与 runWhen 的源码级解析【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios本文基于 Axios 官方文档《Interceptors》法语版展开系统讲解请求/响应拦截器的注册、移除、默认执行模型、同步拦截器的错误语义、runWhen条件执行以及最容易出错的执行顺序问题并结合仓库源码lib/core/InterceptorManager.js与lib/core/Axios.js的实现解释上述每一种行为背后的调用链与判定逻辑。读完后你可以准确预测任意一组拦截器的执行次序并正确使用synchronous、runWhen、eject、clear等选项完成认证头注入、日志、响应解包等生产级任务。拦截器是什么一个可拦截请求与响应的钩子机制拦截器是 Axios 中用于拦截并修改 HTTP 请求与响应的钩子机制其作用与 Express.js 的中间件middleware非常相似拦截器函数在请求发出之前、响应接收之前被调用。它适用于日志记录、请求头修改如统一注入 Token、响应数据转换、错误统一处理等任务。Axios 实例在构造时就会创建两个独立的拦截器管理器分别管理请求与响应两条链// lib/core/Axios.js 构造函数节选 this.interceptors { request: new InterceptorManager(), response: new InterceptorManager(), };基础用法如下use()的第二个参数是可选的失败处理器// 添加请求拦截器 axios.interceptors.request.use( function (config) { // 在请求发出前执行操作 return config; }, function (error) { // 处理请求阶段的错误 return Promise.reject(error); } ); // 添加响应拦截器 axios.interceptors.response.use( function (response) { // 所有 2xx 状态码都会触发该函数 // 处理响应数据 return response; }, function (error) { // 所有非 2xx 状态码都会触发该函数 // 处理响应错误 return Promise.reject(error); } );从源码看use()会把fulfilled、rejected以及选项中的synchronous、runWhen打包成一个 handler 对象push进this.handlers数组并返回一个自增的数字 ID这个 ID 就是后续eject()移除该拦截器的凭据见 InterceptorManager.jsuse(fulfilled, rejected, options) { const handler { fulfilled, rejected, synchronous: options ? options.synchronous : false, runWhen: options ? options.runWhen : null, }; // ... this.handlers.push(handler); return id; // 返回用于后续移除的 ID }移除拦截器eject 与 clear可以通过eject()移除任意单个拦截器也可以通过clear()清空某一类请求或响应的全部拦截器。移除单个拦截器时必须保存use()的返回值// 请求拦截器 const myInterceptor axios.interceptors.request.use(function () { /* ... */ }); axios.interceptors.request.eject(myInterceptor); // 响应拦截器 const myInterceptor axios.interceptors.response.use(function () { /* ... */ }); axios.interceptors.response.eject(myInterceptor);移除某个实例上的全部拦截器const instance axios.create(); instance.interceptors.request.use(function () { /* ... */ }); instance.interceptors.request.clear(); // 清空该实例的请求拦截器 instance.interceptors.response.use(function () { /* ... */ }); instance.interceptors.response.clear(); // 清空该实例的响应拦截器源码实现上有两个值得注意的细节见 InterceptorManager.jseject(id)并不会立即从数组中删除元素而是把对应位置置为null打洞。真正的清理发生在遍历完成后forEach()跳过null项迭代结束时trimHandlers()才把这些空槽位从数组尾部收缩掉。源码注释明确指出这是为了避免在forEach正在基于长度快照遍历时复用下标从而保证迭代安全clear()只是把this.handlers重置为空数组并同步内部索引。同时源码对handlers被用户代码置为 nullish这种情况做了容错处理——countHandlers/trimHandlers都会把它按空栈处理而不会抛错。这也意味着eject之后、下一次请求完成遍历之前被移除的拦截器只是逻辑失效其 ID 与数组下标的映射由内部handlerEntriesMap 维护如果在clear或外部直接替换handlers之后再用旧 ID 调用eject源码会检测到该 ID 已失效并静默忽略不会误删其他拦截器。默认异步执行模型与synchronous选项请求拦截器默认被视为异步的。这个默认行为有一个实际影响当主线程被阻塞时请求的发出可能产生可感知的延迟——Axios 会为拦截器在背后创建 Promise并把你的请求排到调用栈的末尾微任务队列之后执行。如果你的请求拦截器实际上都是同步代码可以通过选项对象传入synchronous: true告诉 Axios 以同步方式执行拦截器代码避免这种延迟axios.interceptors.request.use( function (config) { config.headers.test I am only a header!; return config; }, null, { synchronous: true } );在 Axios.js 的_request中这一行为有清晰的两条分支// 过滤掉被 runWhen 跳过的拦截器 const requestInterceptorChain []; let synchronousRequestInterceptors true; this.interceptors.request.forEach(function (interceptor) { // runWhen 返回 false 的拦截器直接跳过 if (typeof interceptor.runWhen function interceptor.runWhen(config) false) { return; } // 只要有一个拦截器不是同步标记的整条链就走异步路径 synchronousRequestInterceptors synchronousRequestInterceptors interceptor.synchronous; // ...组装 requestInterceptorChain }); // 分支一异步路径 —— 串接 Promise 链 if (!synchronousRequestInterceptors) { const chain [dispatchRequest.bind(this), undefined]; chain.unshift(...requestInterceptorChain); chain.push(...responseInterceptorChain); promise Promise.resolve(config); while (i len) { promise promise.then(chain[i], chain[i]); } return promise; } // 分支二同步路径 —— 用 while 循环直接调用最后才进入 Promise 链 // 只有响应拦截器仍然按 Promise 链串接两个要点同步是全有或全无的只有当参与执行的所有请求拦截器都显式标记了synchronous: true时才走同步分支只要有一个没标记或未标记默认视为异步整条请求链都走 Promise 链。浏览器端测试 tests/browser/interceptors.browser.test.js 中 should execute asynchronously when not all interceptors are explicitly flagged as synchronous 用例正是对这一规则的验证dispatchRequest的位置在异步路径中真正发起 HTTP 请求的dispatchRequest被固定在拦截器链的中枢位置请求拦截器在它之前、响应拦截器在它之后在同步路径中请求拦截器先用循环同步执行完再调用dispatchRequest响应拦截器依旧以promise.then串接。同步拦截器的错误处理语义这是使用synchronous: true时最容易被误解的部分。当某个同步请求拦截器抛出错误时Axios 会调用该拦截器绑定的onRejected即use()的第二个参数处理器并停止执行剩余的请求拦截器。此后的走向取决于该处理器如何返回处理器正常返回包括返回undefined、返回已解决的 Promise甚至显式返回一个新 config错误被视为已处理Axios 会使用最后一次有效的配置发出请求。注意处理器的返回值不会替换这份配置处理器被省略或处理器本身抛错 / 返回被拒绝的 Promise请求不会被发出终止性错误继续向响应阶段的拒绝拦截器传播即进入响应链的rejected处理。一个典型的校验失败则拒绝请求示例axios.interceptors.request.use( function validate(config) { if (!config.headers.has(Authorization)) { throw new Error(Authorization is required); } return config; }, function rejectInvalidRequest(error) { // 让错误继续向上传播阻止请求发出 return Promise.reject(error); }, { synchronous: true } );而一个只记录、不阻断的处理器则保持原有行为继续发请求axios.interceptors.request.use( function prepare(config) { throw new Error(Optional preparation failed); }, function logPreparationFailure(error) { console.warn(error); // 正常返回Axios 用最后的有效配置照常发送请求 }, { synchronous: true } );源码中对应的是 Axios.js 同步分支的try/catchonFulfilled抛错后若没有onRejected直接Promise.reject(error)并跳出循环请求不会发出若onRejected正常返回或返回 thenable则基于**当前的newConfig即最后一次有效配置**调用dispatchRequest。这一语义有完整的浏览器端测试佐证见 tests/browser/interceptors.browser.test.jsshould reject with the original error and not send when a synchronous interceptor has no onRejected—— 无处理器时原始错误上抛且requests长度为 0请求未发出should not send when a synchronous interceptor onRejected returns a rejected promise—— 处理器返回拒绝 Promise 时同样不发送should send with the last valid config when a synchronous interceptor onRejected resolves—— 处理器即使返回一个包含{ url: /bar }的已解决 Promise该返回值也不会替换配置请求仍按最后有效配置发出。用runWhen实现条件执行如果希望某个拦截器只在满足运行时条件时才执行可以在选项对象中提供runWhen函数当且仅当runWhen返回false时拦截器不会执行。该函数以当前请求的 config 对象作为参数调用你可以在自己的代码中用闭包绑定额外参数。这对只想在特定条件下才执行的异步请求拦截器特别有用。function onGetCall(config) { return config.method get; } axios.interceptors.request.use( function (config) { config.headers.test special get headers; return config; }, null, { runWhen: onGetCall } );从源码看runWhen的判定发生在组装请求链的阶段Axios.jsif (typeof interceptor.runWhen function interceptor.runWhen(config) false) { return; // 直接从链中剔除 }也就是说runWhen被判定为false的拦截器在构建链时就被过滤掉根本不参与执行因此它也不会影响是否全部同步的判定。测试用例 does not run async interceptor if runWhen resolves to false 验证了一个关键联动一个带runWhen返回 false 的 GET 请求的异步拦截器被剔除后剩余的唯一同步拦截器使整条链按同步路径执行——asyncFlag在请求发出前仍为false。执行顺序请求链 LIFO响应链 FIFO这是拦截器最核心的行为约定也是文档中最强的告警项请求拦截器与响应拦截器的执行顺序是相反的。请求拦截器按逆序执行LIFO — last in, first out最后添加的请求拦截器最先执行。 响应拦截器按添加顺序执行FIFO — first in, first out最先添加的响应拦截器最先执行。官方示例展示了三个请求拦截器 三个响应拦截器下的完整执行顺序const instance axios.create(); const interceptor (id) (base) { console.log(id); return base; }; instance.interceptors.request.use(interceptor(Request Interceptor 1)); instance.interceptors.request.use(interceptor(Request Interceptor 2)); instance.interceptors.request.use(interceptor(Request Interceptor 3)); instance.interceptors.response.use(interceptor(Response Interceptor 1)); instance.interceptors.response.use(interceptor(Response Interceptor 2)); instance.interceptors.response.use(interceptor(Response Interceptor 3)); // 控制台输出 // Request Interceptor 3 // Request Interceptor 2 // Request Interceptor 1 // [HTTP 请求在此发出] // Response Interceptor 1 // Response Interceptor 2 // Response Interceptor 3源码里这两条链的组装方式完全对应了这个顺序Axios.jsthis.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { // ... requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected); // 头部插入 → LIFO }); const responseInterceptorChain []; this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); // 尾部追加 → FIFO });请求拦截器用unshift头插、响应拦截器用push尾插两者结合就产生了请求倒序、响应正序的洋葱模型请求阶段像剥洋葱一样由新到旧响应阶段再由旧到新保证每个拦截器的请求侧动作和响应侧动作能对称地包住中间的dispatchRequest。一个历史兼容细节源码中还存在transitional.legacyInterceptorReqResOrdering开关Axios.js开启时请求拦截器改用push组装即恢复 v0.x 时代的旧顺序。从源码结构看这是为了迁移场景提供的过渡选项在文档未另行声明的前提下新代码无需依赖该行为默认应以上述 LIFO/FIFO 顺序为准。多个拦截器共存时的行为规则可以对同一请求或同一响应注册多个拦截器。同一条链上多个拦截器共存时按以下规则执行每个拦截器都会被执行请求拦截器按逆序LIFO执行响应拦截器按添加顺序FIFO执行只有最后一个拦截器的结果会作为链的最终结果返回每个拦截器接收的是其前驱拦截器的返回值形成流水线传递当某个成功处理器抛出异常时下一个成功拦截器不会被调用下一个失败rejected拦截器会被调用异常被捕获rejected 处理器正常返回之后后续的成功拦截器会重新被调用——与普通 Promise 链的catch/then语义一致。最后一条规则本质上来自拦截器链的 Promise 串接方式异步路径中每一对(fulfilled, rejected)都被依次promise.then(chain[i], chain[i])挂接Axios.js因此链的错误传播、恢复、继续执行完全遵循标准 Promise 语义rejected处理器消化错误后链会从其后的fulfilled处理器处继续。文档还提示如需深入理解拦截器的完整行为矩阵可以阅读仓库中随附的测试用例本仓库的 tests/browser/interceptors.browser.test.js 覆盖了默认异步、显式同步/异步标记、runWhen与同步/异步联动、同步拦截器各类错误路径等场景是验证本文所述行为最可靠的依据。小结一张表看懂拦截器关键行为主题行为源码/测试依据注册与 IDuse(fulfilled, rejected, options)返回自增数字 IDInterceptorManager.js移除单个eject(id)置空槽位遍历结束后统一收缩失效 ID 静默忽略InterceptorManager.js清空clear()重置为空数组容忍handlers被外部置空InterceptorManager.js默认异步只要有一个请求拦截器未标记同步整条链走 Promise 链Axios.js同步路径全部synchronous: true时请求拦截器同步执行无微任务延迟Axios.js同步错误语义onRejected正常返回 → 用最后有效配置发送省略/抛错/返回拒绝 Promise → 不发送错误进入响应拒绝链Axios.js、测试runWhen仅当返回false时拦截器被跳过且不影响同步判定Axios.js执行顺序请求 LIFOunshift响应 FIFOpushAxios.js【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考