Axios 核心技术深度解析:从源码分析到自定义实现

导读:本文基于 Axios 官方源码(v1.x系列),结合 ECMA-262 规范与 MDN 文档,系统讲解 Axios 的底层实现原理、拦截器机制、请求取消等核心技术。文章包含完整可运行的示例代码,涵盖从基础使用到源码级自定义实现的完整路径,适合希望深入理解 HTTP 库设计的前端工程师阅读。


目录

  • 零、导读与学习价值
    • [0.1 示例覆盖清单](#0.1 示例覆盖清单)
    • [0.2 核心名词速查](#0.2 核心名词速查)
    • [0.3 为什么要学本篇](#0.3 为什么要学本篇)
    • [0.4 建议练习路线(八步递进)](#0.4 建议练习路线(八步递进))
  • [一、Axios 源码架构分析](#一、Axios 源码架构分析)
    • [1.1 名词解释](#1.1 名词解释)
    • [1.2 概念与底层原理](#1.2 概念与底层原理)
    • [1.3 源码目录结构](#1.3 源码目录结构)
    • [1.4 Axios 运行流程](#1.4 Axios 运行流程)
    • [1.5 官方入口与入门实现的差异](#1.5 官方入口与入门实现的差异)
    • [1.6 建议精读的核心文件](#1.6 建议精读的核心文件)
  • [二、自定义实现 Axios 核心](#二、自定义实现 Axios 核心)
  • 三、拦截器机制深度剖析
    • [3.1 拦截器核心概念](#3.1 拦截器核心概念)
    • [3.2 拦截器实现原理](#3.2 拦截器实现原理)
    • [3.3 拦截器实战应用](#3.3 拦截器实战应用)
  • 四、总结
    • [4.1 知识点回顾](#4.1 知识点回顾)
    • [4.2 高频面试题速查](#4.2 高频面试题速查)
    • [4.3 学习建议](#4.3 学习建议)

零、导读与学习价值

0.1 示例覆盖清单

本文完整覆盖以下知识点与可运行示例:

示例 核心知识点 本文章节
Axios 源码架构剖析 目录结构、createInstance + utils.extend §1
核心源码精读 Axios.js 的 request 链、dispatchRequestsettle §1
创建 axios 函数示例 bind 委托、axios 本质不是 Axios 实例 §2.1
请求发送与 Promise 封装示例 dispatchRequest + XMLHttpRequest §2.2
请求配置项示例 baseURLparamsheadersdata §2.3
响应结果处理示例 响应对象结构、4xx/5xx 转 reject §2.4
超时设置示例 xhr.timeoutECONNABORTED 错误码 §2.5
请求取消示例 CancelToken + xhr.abort §2.6
便捷方法示例 get / post 挂载到 instance §2.7
拦截器示例 InterceptorManager、Promise 链 §3
拦截器实战封装示例 token、loading、统一错误处理组合 §3.3

0.2 核心名词速查

术语 一句话解释
适配器模式 Axios 通过适配器屏蔽浏览器(XMLHttpRequest)与 Node.js(http 模块)的差异
拦截器 在请求发送前/响应接收后插入自定义逻辑的中间件机制
CancelToken 基于 Promise 状态控制实现请求取消的机制
责任链模式 拦截器采用的设计模式,让多个处理器依次处理请求/响应
Promise 链 Axios 通过 Promise 链实现拦截器的异步串联调用

0.3 为什么要学本篇

  • 工程实践价值:掌握 Axios 设计模式,可应用到其他 HTTP 库或中间件系统的设计
  • 面试加分项:源码级理解是高级前端工程师的必备能力,面试中能深入讨论实现细节
  • 架构设计能力:学习 Axios 的适配器设计、拦截器机制,提升系统架构思维
  • 调试能力:理解底层原理能快速定位请求问题,提升开发效率

与上一章的衔接 :若你已会用 Axios(axios(config)create、拦截器、all),本篇回答 为什么这样设计 ,并带你 从零手写迷你版。建议先能独立发 GET/POST、写拦截器,再按 §0.4 的八步路线递进实现。

0.4 建议练习路线(八步递进)

#mermaid-svg-KJxV5tXfTJr02PMw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KJxV5tXfTJr02PMw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KJxV5tXfTJr02PMw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KJxV5tXfTJr02PMw .error-icon{fill:#552222;}#mermaid-svg-KJxV5tXfTJr02PMw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KJxV5tXfTJr02PMw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KJxV5tXfTJr02PMw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KJxV5tXfTJr02PMw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KJxV5tXfTJr02PMw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KJxV5tXfTJr02PMw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KJxV5tXfTJr02PMw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KJxV5tXfTJr02PMw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KJxV5tXfTJr02PMw .marker.cross{stroke:#333333;}#mermaid-svg-KJxV5tXfTJr02PMw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KJxV5tXfTJr02PMw p{margin:0;}#mermaid-svg-KJxV5tXfTJr02PMw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-KJxV5tXfTJr02PMw .cluster-label text{fill:#333;}#mermaid-svg-KJxV5tXfTJr02PMw .cluster-label span{color:#333;}#mermaid-svg-KJxV5tXfTJr02PMw .cluster-label span p{background-color:transparent;}#mermaid-svg-KJxV5tXfTJr02PMw .label text,#mermaid-svg-KJxV5tXfTJr02PMw span{fill:#333;color:#333;}#mermaid-svg-KJxV5tXfTJr02PMw .node rect,#mermaid-svg-KJxV5tXfTJr02PMw .node circle,#mermaid-svg-KJxV5tXfTJr02PMw .node ellipse,#mermaid-svg-KJxV5tXfTJr02PMw .node polygon,#mermaid-svg-KJxV5tXfTJr02PMw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KJxV5tXfTJr02PMw .rough-node .label text,#mermaid-svg-KJxV5tXfTJr02PMw .node .label text,#mermaid-svg-KJxV5tXfTJr02PMw .image-shape .label,#mermaid-svg-KJxV5tXfTJr02PMw .icon-shape .label{text-anchor:middle;}#mermaid-svg-KJxV5tXfTJr02PMw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KJxV5tXfTJr02PMw .rough-node .label,#mermaid-svg-KJxV5tXfTJr02PMw .node .label,#mermaid-svg-KJxV5tXfTJr02PMw .image-shape .label,#mermaid-svg-KJxV5tXfTJr02PMw .icon-shape .label{text-align:center;}#mermaid-svg-KJxV5tXfTJr02PMw .node.clickable{cursor:pointer;}#mermaid-svg-KJxV5tXfTJr02PMw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KJxV5tXfTJr02PMw .arrowheadPath{fill:#333333;}#mermaid-svg-KJxV5tXfTJr02PMw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KJxV5tXfTJr02PMw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KJxV5tXfTJr02PMw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KJxV5tXfTJr02PMw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KJxV5tXfTJr02PMw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KJxV5tXfTJr02PMw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KJxV5tXfTJr02PMw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KJxV5tXfTJr02PMw .cluster text{fill:#333;}#mermaid-svg-KJxV5tXfTJr02PMw .cluster span{color:#333;}#mermaid-svg-KJxV5tXfTJr02PMw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-KJxV5tXfTJr02PMw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KJxV5tXfTJr02PMw rect.text{fill:none;stroke-width:0;}#mermaid-svg-KJxV5tXfTJr02PMw .icon-shape,#mermaid-svg-KJxV5tXfTJr02PMw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KJxV5tXfTJr02PMw .icon-shape p,#mermaid-svg-KJxV5tXfTJr02PMw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KJxV5tXfTJr02PMw .icon-shape .label rect,#mermaid-svg-KJxV5tXfTJr02PMw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KJxV5tXfTJr02PMw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KJxV5tXfTJr02PMw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KJxV5tXfTJr02PMw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 01 创建函数
02 XHR+Promise
03 配置合并
04 响应对象
05 超时
06 取消
07 get/post
08 拦截器
对照官方 lib 源码

【代码注释】

  • 每一步只扩展当前版本的 axios.js,用配套 HTML 在浏览器里验证;跳步会导致 bug 难定位。
  • 步骤 01 完成后,可再引入官方打包的 axios.js,对比 axiosaxios.defaults 的对象结构。
  • 无模块化构建时,用 IIFE 把迷你 axios 挂到 window.axios,在控制台直接调试。
  • 步骤 08 做完后,打开官方 lib/core/InterceptorManager.jsAxios.js#request,对照 Promise 链如何拼出来。
步骤 本步新增能力 与官方源码大致对应
01 createInstance + bind lib/axios.js
02 dispatchRequest、XHR adapters/xhr.js + dispatchRequest.js
03 配置合并(入门用 Object.assign mergeConfig.js
04 resolve/reject 响应结构 settle.js + 响应体组装
05 ontimeout xhr 适配器内
06 CancelToken cancel/CancelToken.js
07 get/post 挂到 instance Axios.js 原型方法
08 拦截器链 InterceptorManager.js + Axios.js request

【实战要点】

  • 入门版 createInstance 往往只做到 bind,尚未 extend 原型方法;§2.1 的完整版用 for...ingetOwnPropertyNames 补全,更接近官方 utils.extend
  • 读源码时用 IDE「跳转到定义」从 axios(config) 跟到 Axios.prototype.request → 拦截器链 → dispatchRequest

【面试考点】

  • 手写 axios 最小要实现哪几个函数/类?(createInstanceAxios#requestdispatchRequest、XHR 适配器、可选 InterceptorManager
  • 官方 createInstance 与「只做 bind」的入门版差在哪?(utils.extend 复制原型方法与实例属性、instance.create 递归合并默认配置)

一、Axios 源码架构分析

1.1 名词解释

  • axios 实例 :对外暴露的「可调用函数」,内部 this 绑定到 Axios 上下文,拥有 defaultsinterceptorsget/post 等方法。
  • 适配器(adapter) :真正发 HTTP 的模块;浏览器为 xhr.js,Node 为 http.js,由 dispatchRequest 按环境选择。
  • dispatchRequest :拦截器链执行完毕后,调用适配器并走 transformRequest / transformResponse 的函数。
  • mergeConfig :把「实例默认配置」与「本次请求配置」按字段策略深度合并,避免 headers 被浅拷贝覆盖丢字段。
  • settle :根据 HTTP status 决定 Promise 走 resolve 还是 reject(2xx 为成功,其余为 AxiosError)。
  • InterceptorManager :维护 handlers 数组,use 注册、eject 置空,供 Axios#request 拼 Promise 链。

Axios 本质不是「直接 new 出来的 axios」 ,而是一个通过 bind 委托了 Axios.prototype.request 的函数。调用 axios(config) 时,执行的是实例上的 requestthis 指向持有 defaultsinterceptorsAxios 对象。

1.2 概念与底层原理

官方一次请求的完整生命周期(见 axios 仓库 AGENTS.md)可概括为:

  1. 用户调用 axios(url)axios(config)
  2. requestmergeConfig(this.defaults, config),再校验 transitionalparamsSerializer 等;
  3. buildFullPath + buildURL 拼出最终 URL;
  4. 请求拦截器 (LIFO)依次改写 config
  5. dispatchRequest 选适配器 → transformRequest → 发网络请求;
  6. 适配器返回后 settle 根据状态码 resolve/reject;
  7. 响应拦截器 (FIFO)处理 responseerror
  8. 最外层 Promise 交给业务 then/catchawait

为何 axios 是函数而不是 class 实例?

JavaScript 允许函数带属性。createInstancebind(Axios.prototype.request, context) 得到可调用的 instance,再用 utils.extendget/postdefaultsinterceptors 挂到同一对象上,于是 API 同时支持 axios({ url })axios.get(url),且 axios.create() 可派生多实例(多 baseURL、多超时策略)。

mergeConfig 在 v1.x 的分层策略Configuration Merging):不同字段用不同合并规则------例如 urlmethod 以本次请求为准;baseURLtimeouttransformRequest 等默认「请求级覆盖实例级」;headers 常做深度合并。手写版用 Object.assign 是简化版,生产库必须处理 headers.common / headers.post 等嵌套,否则 POST 会丢全局 Content-Type
xhr/http 适配器 dispatchRequest 拦截器链 Axios 业务代码 xhr/http 适配器 dispatchRequest 拦截器链 Axios 业务代码 #mermaid-svg-RpLKFVCbezen8fte{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RpLKFVCbezen8fte .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RpLKFVCbezen8fte .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RpLKFVCbezen8fte .error-icon{fill:#552222;}#mermaid-svg-RpLKFVCbezen8fte .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RpLKFVCbezen8fte .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RpLKFVCbezen8fte .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RpLKFVCbezen8fte .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RpLKFVCbezen8fte .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RpLKFVCbezen8fte .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RpLKFVCbezen8fte .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RpLKFVCbezen8fte .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RpLKFVCbezen8fte .marker.cross{stroke:#333333;}#mermaid-svg-RpLKFVCbezen8fte svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RpLKFVCbezen8fte p{margin:0;}#mermaid-svg-RpLKFVCbezen8fte .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RpLKFVCbezen8fte text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-RpLKFVCbezen8fte .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-RpLKFVCbezen8fte .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-RpLKFVCbezen8fte .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-RpLKFVCbezen8fte .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-RpLKFVCbezen8fte #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-RpLKFVCbezen8fte .sequenceNumber{fill:white;}#mermaid-svg-RpLKFVCbezen8fte #sequencenumber{fill:#333;}#mermaid-svg-RpLKFVCbezen8fte #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-RpLKFVCbezen8fte .messageText{fill:#333;stroke:none;}#mermaid-svg-RpLKFVCbezen8fte .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RpLKFVCbezen8fte .labelText,#mermaid-svg-RpLKFVCbezen8fte .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-RpLKFVCbezen8fte .loopText,#mermaid-svg-RpLKFVCbezen8fte .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-RpLKFVCbezen8fte .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-RpLKFVCbezen8fte .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-RpLKFVCbezen8fte .noteText,#mermaid-svg-RpLKFVCbezen8fte .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-RpLKFVCbezen8fte .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RpLKFVCbezen8fte .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RpLKFVCbezen8fte .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RpLKFVCbezen8fte .actorPopupMenu{position:absolute;}#mermaid-svg-RpLKFVCbezen8fte .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-RpLKFVCbezen8fte .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RpLKFVCbezen8fte .actor-man circle,#mermaid-svg-RpLKFVCbezen8fte line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-RpLKFVCbezen8fte :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} axios(config) mergeConfig 请求拦截器 LIFO config adapter(config) response 响应拦截器 FIFO Promise

【代码注释】

序列图把「配置合并 → 拦截器 → 适配器」串成一条线。追问「axios.getaxios(config) 走同一套吗」时答:便捷方法最终仍调用 request,只是预先填好 methoddata/params 的位置。

1.3 源码目录结构

text 复制代码
axios/
├── /dist/                     # 项目输出目录
├── /lib/                      # 项目源码目录
│ ├── adapters/                # 定义请求的适配器 xhr、http
│ │ ├── http.js                # 实现 http 适配器(包装 http 包)
│ │ └── xhr.js                 # 实现 xhr 适配器(包装 xhr 对象)
│ ├── cancel/                  # 定义取消功能
│ │ ├── CancelToken.js         # 取消令牌实现
│ │ ├── CanceledError.js       # 取消错误类
│ │ └── isCancel.js            # 判断是否为取消错误
│ ├── core/                    # 一些核心功能
│ │ ├── Axios.js               # axios 的核心主类
│ │ ├── dispatchRequest.js     # 用来调用 http 请求适配器方法发送请求的函数
│ │ ├── InterceptorManager.js  # 拦截器的管理器
│ │ ├── settle.js              # 根据 http 响应状态,改变 Promise 的状态
│ │ └── mergeConfig.js         # 配置合并
│ ├── helpers/                 # 一些辅助方法
│ │ ├── bind.js                # 函数绑定工具
│ │ ├── buildURL.js            # URL 参数构建
│ │ └── cookies.js             # Cookie 处理
│ ├── defaults/                # axios 的默认配置
│ ├── axios.js                 # 对外暴露接口
│ └── utils.js                 # 公用工具
├── package.json               # 项目信息
├── index.d.ts                 # 配置 TypeScript 的声明文件
└── index.js                   # 入口文件

【代码注释】 这是 Axios 完整的源码目录结构,核心模块包括:adapters/(适配器层,实现跨环境)、core/(核心逻辑,Axios 类与拦截器)、cancel/(请求取消机制)。理解目录结构有助于快速定位源码位置,如修改拦截器逻辑看 InterceptorManager.js,了解适配器差异对比 xhr.jshttp.js

1.4 Axios 运行流程

#mermaid-svg-iKAePAbtyuPBGgWt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iKAePAbtyuPBGgWt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iKAePAbtyuPBGgWt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iKAePAbtyuPBGgWt .error-icon{fill:#552222;}#mermaid-svg-iKAePAbtyuPBGgWt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iKAePAbtyuPBGgWt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iKAePAbtyuPBGgWt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iKAePAbtyuPBGgWt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iKAePAbtyuPBGgWt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iKAePAbtyuPBGgWt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iKAePAbtyuPBGgWt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iKAePAbtyuPBGgWt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iKAePAbtyuPBGgWt .marker.cross{stroke:#333333;}#mermaid-svg-iKAePAbtyuPBGgWt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iKAePAbtyuPBGgWt p{margin:0;}#mermaid-svg-iKAePAbtyuPBGgWt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iKAePAbtyuPBGgWt .cluster-label text{fill:#333;}#mermaid-svg-iKAePAbtyuPBGgWt .cluster-label span{color:#333;}#mermaid-svg-iKAePAbtyuPBGgWt .cluster-label span p{background-color:transparent;}#mermaid-svg-iKAePAbtyuPBGgWt .label text,#mermaid-svg-iKAePAbtyuPBGgWt span{fill:#333;color:#333;}#mermaid-svg-iKAePAbtyuPBGgWt .node rect,#mermaid-svg-iKAePAbtyuPBGgWt .node circle,#mermaid-svg-iKAePAbtyuPBGgWt .node ellipse,#mermaid-svg-iKAePAbtyuPBGgWt .node polygon,#mermaid-svg-iKAePAbtyuPBGgWt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iKAePAbtyuPBGgWt .rough-node .label text,#mermaid-svg-iKAePAbtyuPBGgWt .node .label text,#mermaid-svg-iKAePAbtyuPBGgWt .image-shape .label,#mermaid-svg-iKAePAbtyuPBGgWt .icon-shape .label{text-anchor:middle;}#mermaid-svg-iKAePAbtyuPBGgWt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iKAePAbtyuPBGgWt .rough-node .label,#mermaid-svg-iKAePAbtyuPBGgWt .node .label,#mermaid-svg-iKAePAbtyuPBGgWt .image-shape .label,#mermaid-svg-iKAePAbtyuPBGgWt .icon-shape .label{text-align:center;}#mermaid-svg-iKAePAbtyuPBGgWt .node.clickable{cursor:pointer;}#mermaid-svg-iKAePAbtyuPBGgWt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iKAePAbtyuPBGgWt .arrowheadPath{fill:#333333;}#mermaid-svg-iKAePAbtyuPBGgWt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iKAePAbtyuPBGgWt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iKAePAbtyuPBGgWt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iKAePAbtyuPBGgWt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iKAePAbtyuPBGgWt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iKAePAbtyuPBGgWt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iKAePAbtyuPBGgWt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iKAePAbtyuPBGgWt .cluster text{fill:#333;}#mermaid-svg-iKAePAbtyuPBGgWt .cluster span{color:#333;}#mermaid-svg-iKAePAbtyuPBGgWt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iKAePAbtyuPBGgWt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iKAePAbtyuPBGgWt rect.text{fill:none;stroke-width:0;}#mermaid-svg-iKAePAbtyuPBGgWt .icon-shape,#mermaid-svg-iKAePAbtyuPBGgWt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iKAePAbtyuPBGgWt .icon-shape p,#mermaid-svg-iKAePAbtyuPBGgWt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iKAePAbtyuPBGgWt .icon-shape .label rect,#mermaid-svg-iKAePAbtyuPBGgWt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iKAePAbtyuPBGgWt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iKAePAbtyuPBGgWt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iKAePAbtyuPBGgWt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 调用 axios/config
合并配置项

defaults + config
请求拦截器

后进先出 LIFO
分发请求

dispatchRequest
适配器选择

xhr/http
发送网络请求
响应拦截器

先进先出 FIFO
返回响应数据

【代码注释】 此流程图展示了一个完整请求的生命周期:从调用 axios 函数开始,经过配置合并、请求拦截器(LIFO顺序)、请求分发、适配器选择(浏览器用XHR、Node用http模块)、网络请求、响应拦截器(FIFO顺序),最终返回响应数据。拦截器的执行顺序是关键------请求拦截器后添加的先执行,响应拦截器先添加的先执行。

流程说明

  1. 配置合并:将用户配置与默认配置合并
  2. 请求拦截器:按照后进先出(LIFO)顺序执行
  3. 请求分发 :调用 dispatchRequest 选择适配器
  4. 网络请求:底层使用 XMLHttpRequest 或 http 模块
  5. 响应拦截器:按照先进先出(FIFO)顺序执行
  6. 数据返回:返回格式化后的响应数据

【实战要点】

  • 断点建议:Axios.jsrequestdispatchRequestxhr.jsonload
  • 请求拦截器里改 config.urlreturn config,否则后续拿到 undefined

【面试考点】

  • 画出从 axios(config) 到 XHR send 的调用栈(至少 4 层)。
  • 为何响应拦截器用 FIFO、请求拦截器用 LIFO?

1.5 官方入口与入门实现的差异

官方 createInstancelib/axios.js):

javascript 复制代码
function createInstance(defaultConfig) {
  const context = new Axios(defaultConfig);
  const instance = bind(Axios.prototype.request, context);
  utils.extend(instance, Axios.prototype, context, { allOwnKeys: true });
  utils.extend(instance, context, null, { allOwnKeys: true });
  instance.create = function (instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };
  return instance;
}
const axios = createInstance(defaults);

【代码注释】

  • 入门版 (§2.1)通常只做到 bind 返回 instance,尚未 extend 原型方法与 get/post
  • instance.create 对应业务侧的 axios.create,内部 mergeConfig(defaultConfig, instanceConfig) 合并默认配置后 createInstance
  • 导出时还挂载 CancelTokenisAxiosErrorallspread 等静态工具(见文件后半段)。
  • 无模块化时可将 createInstance 包在 IIFE 里挂到 window.axios,便于在 HTML 里直接调试。

1.6 建议精读的核心文件

文件 作用
core/Axios.js request:合并配置 → 走拦截器链 → dispatchRequest
core/dispatchRequest.js 选适配器(xhr/http),发真实请求
core/mergeConfig.js 深度合并 defaults 与本次 config
core/settle.js 根据 status 决定 resolve 还是 reject
core/InterceptorManager.js use / eject,handlers 数组
helpers/buildURL.js baseURL + url + params 拼完整地址
helpers/combineURLs.jsisAbsoluteURL.js 相对路径与绝对路径拼接规则
adapters/xhr.js 浏览器端 XHR 实现(与 §2.2 手写高度相似)

【代码注释】

  • transformData.jsAxiosHeaders.js 负责请求/响应头与体的转换,进阶阅读即可。
  • dist/axios.min.js 为打包产物,调试原理请读 lib/ 源码。
  • 不必通读 helpers/ 下每一个流式/压缩相关文件,按表内顺序即可建立全局图。

【本章小结】

模块 职责 记忆点
lib/axios.js createInstance、导出 create axios 是 bind 出来的函数
core/Axios.js request、拦截器链 不直接发请求
dispatchRequest + adapters 选 xhr/http、转换数据 跨环境关键
mergeConfig / settle 合并配置、定 Promise 态 4xx 在 onload 里 reject
InterceptorManager use/eject 请求 LIFO、响应 FIFO

记忆口诀"函数门面 request 心,合并配置再拦截;适配器里真发请求,settle 定成败。"

【面试考点】

Q1:从 axios(config) 到网络发出,至少经过哪四层?

A:createInstance 得到的函数 → Axios#requestmergeConfig)→ 请求拦截器链 → dispatchRequest 选适配器并 xhr.send。响应方向再走响应拦截器。答不出 mergeConfigdispatchRequest 说明只背了 API。

Q2:mergeConfig 为什么不能简单 Object.assign

A:headerscommongetpost 等桶,浅合并会覆盖整棵子树;url/method 又必须每次请求独立。官方对每类字段有 valueFromConfig2defaultToConfig2mergeDeepProperties 等策略。

Q3:浏览器端 axios 底层是 Fetch 还是 XHR?

A:默认 XHRadapters/xhr.js)。因此支持 onUploadProgressxhr.timeout;与 Fetch 的 AbortController 是另一套取消模型(v1.6+ 也支持 signal)。


二、自定义实现 Axios 核心

以下 §2.1~§2.7 与 §0.4 八步路线一一对应;每节附带可保存为 .html 的完整示例,在本目录下打开即可运行。

2.1 创建 axios 函数

核心原理:axios 是一个函数,但拥有 Axios 类实例的所有属性和方法。
#mermaid-svg-eXwHG7eHK1ttIcEh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eXwHG7eHK1ttIcEh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eXwHG7eHK1ttIcEh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eXwHG7eHK1ttIcEh .error-icon{fill:#552222;}#mermaid-svg-eXwHG7eHK1ttIcEh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eXwHG7eHK1ttIcEh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eXwHG7eHK1ttIcEh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eXwHG7eHK1ttIcEh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eXwHG7eHK1ttIcEh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eXwHG7eHK1ttIcEh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eXwHG7eHK1ttIcEh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eXwHG7eHK1ttIcEh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eXwHG7eHK1ttIcEh .marker.cross{stroke:#333333;}#mermaid-svg-eXwHG7eHK1ttIcEh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eXwHG7eHK1ttIcEh p{margin:0;}#mermaid-svg-eXwHG7eHK1ttIcEh g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-eXwHG7eHK1ttIcEh g.classGroup text .title{font-weight:bolder;}#mermaid-svg-eXwHG7eHK1ttIcEh .cluster-label text{fill:#333;}#mermaid-svg-eXwHG7eHK1ttIcEh .cluster-label span{color:#333;}#mermaid-svg-eXwHG7eHK1ttIcEh .cluster-label span p{background-color:transparent;}#mermaid-svg-eXwHG7eHK1ttIcEh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eXwHG7eHK1ttIcEh .cluster text{fill:#333;}#mermaid-svg-eXwHG7eHK1ttIcEh .cluster span{color:#333;}#mermaid-svg-eXwHG7eHK1ttIcEh .nodeLabel,#mermaid-svg-eXwHG7eHK1ttIcEh .edgeLabel{color:#131300;}#mermaid-svg-eXwHG7eHK1ttIcEh .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-eXwHG7eHK1ttIcEh .label text{fill:#131300;}#mermaid-svg-eXwHG7eHK1ttIcEh .labelBkg{background:#ECECFF;}#mermaid-svg-eXwHG7eHK1ttIcEh .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-eXwHG7eHK1ttIcEh .classTitle{font-weight:bolder;}#mermaid-svg-eXwHG7eHK1ttIcEh .node rect,#mermaid-svg-eXwHG7eHK1ttIcEh .node circle,#mermaid-svg-eXwHG7eHK1ttIcEh .node ellipse,#mermaid-svg-eXwHG7eHK1ttIcEh .node polygon,#mermaid-svg-eXwHG7eHK1ttIcEh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eXwHG7eHK1ttIcEh .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh g.clickable{cursor:pointer;}#mermaid-svg-eXwHG7eHK1ttIcEh g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-eXwHG7eHK1ttIcEh g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-eXwHG7eHK1ttIcEh .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-eXwHG7eHK1ttIcEh .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-eXwHG7eHK1ttIcEh .dashed-line{stroke-dasharray:3;}#mermaid-svg-eXwHG7eHK1ttIcEh .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-eXwHG7eHK1ttIcEh #compositionStart,#mermaid-svg-eXwHG7eHK1ttIcEh .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #compositionEnd,#mermaid-svg-eXwHG7eHK1ttIcEh .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #dependencyStart,#mermaid-svg-eXwHG7eHK1ttIcEh .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #dependencyStart,#mermaid-svg-eXwHG7eHK1ttIcEh .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #extensionStart,#mermaid-svg-eXwHG7eHK1ttIcEh .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #extensionEnd,#mermaid-svg-eXwHG7eHK1ttIcEh .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #aggregationStart,#mermaid-svg-eXwHG7eHK1ttIcEh .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #aggregationEnd,#mermaid-svg-eXwHG7eHK1ttIcEh .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #lollipopStart,#mermaid-svg-eXwHG7eHK1ttIcEh .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #lollipopEnd,#mermaid-svg-eXwHG7eHK1ttIcEh .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-eXwHG7eHK1ttIcEh .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eXwHG7eHK1ttIcEh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eXwHG7eHK1ttIcEh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eXwHG7eHK1ttIcEh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 委托给实例
Axios
+defaults
+interceptors
+request()
+get()
+post()
axios
+defaults
+interceptors
+request()
+get()
+post()
Function
+call()

【代码注释】 此类图揭示了 axios 的双重身份------既是函数(继承自 Function),又拥有 Axios 类实例的所有属性和方法(通过委托)。这种设计让 axios(config)axios.get(url) 两种调用方式都有效。委托实现通过 bind(Axios.prototype.request, context) 完成,调用 axios() 实际执行 context.request(),且 this 正确指向实例 context

入门示例:基础结构
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Axios 函数创建原理</title>
</head>
<body>
    <script type="module">
        // 定义默认请求配置项
        const defaults = {
            timeout: 0
        };

        // 核心类
        class Axios {
            constructor(instanceConfig) {
                this.defaults = instanceConfig;
            }

            request() {
                console.log('request 方法被调用');
                return Promise.resolve('模拟请求');
            }
        }

        // 函数绑定工具
        function bind(fn, thisArg) {
            return function wrap() {
                // fn 就是 Axios.prototype.request
                // arguments 是伪数组,成员是传递给 axios 函数的参数
                // 就是在调用 Axios.prototype.request,并设置里面的 this 为 Axios 类的一个实例
                return fn.apply(thisArg, arguments);
            }
        }

        /**
         * 创建 axios 函数
         * axios 函数本质不是 Axios 的实例,而是委托给实例的方法
         */
        function createInstance(defaultConfig) {
            // 实例化核心类得到实例
            const context = new Axios(defaultConfig);
            // 创建 axios 函数,绑定 request 方法到 context
            const instance = bind(Axios.prototype.request, context);

            // 将 Axios 实例自身的所有属性都添加到 axios 上
            for (let key in context) {
                instance[key] = context[key];
            }

            // 将 Axios 实例的原型上所有属性都添加到 axios 上
            Object.getOwnPropertyNames(Axios.prototype).forEach(key => {
                instance[key] = Axios.prototype[key].bind(context);
            });

            return instance;
        }

        // 创建 axios
        const axios = createInstance(defaults);

        // 测试
        console.log('=== axios 对象结构 ===');
        console.log('axios 是函数吗?', typeof axios === 'function'); // true
        console.log('axios 是 Axios 实例吗?', axios instanceof Axios); // false
        console.log('axios 有 defaults 属性吗?', 'defaults' in axios); // true
        console.log('axios.defaults:', axios.defaults);

        console.log('\n=== 调用 axios 函数 ===');
        axios().then(result => console.log('请求结果:', result));
    </script>
</body>
</html>

【代码注释】 bind 函数实现了函数委托模式 ------让 axios 函数调用时实际执行 Axios.prototype.request,且内部的 this 正确指向 Axios 实例 context。这种设计让 axios 既是函数又有属性方法,比单纯用类更灵活。市面应用 :Lodash 的 _.throttle、React Redux 的 connect 高阶组件都用类似模式------对外暴露函数,内部委托给实例。

【本章小结】

特性 说明
设计模式 函数委托模式 + 原型继承
axios 本质 函数(通过 bind 创建)
属性来源 从 Axios 实例复制而来
方法来源 从 Axios.prototype.bind(context) 而来

记忆口诀"axios 是函数,属性实例绑;方法原型来,this 指实例"

【面试考点】

Q1:为什么 axios 既是函数又是对象?

A:这是通过函数委托模式 实现的------createInstance 中用 bind(Axios.prototype.request, context) 创建函数,再把实例属性(defaultsinterceptors)和原型方法(getpost)挂到函数上。技术上利用了 JS 中"函数也是对象"的特性。追问"为什么不直接用类"时答:函数更灵活,支持 axios(config)axios.get(url) 两种调用方式,API 更简洁。


2.2 实现请求发送与 Promise 封装

核心原理 :基于 XMLHttpRequest 封装,返回 Promise 对象支持链式调用。

入门示例:基础请求发送
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Axios 请求发送与 Promise 封装</title>
</head>
<body>
    <h1>Axios 请求发送测试</h1>
    <div id="result"></div>
    
    <script type="module">
        // 定义默认请求配置项
        const defaults = {
            timeout: 0,
            responseType: 'json'
        };

        /**
         * 发送 ajax 请求
         * @param {object} config 请求配置项
         * @returns {Promise}
         */
        function dispatchRequest(config) {
            return new Promise((resolve, reject) => {
                // 创建 xhr 对象
                const xhr = new XMLHttpRequest();
                // 设置响应类型
                xhr.responseType = config.responseType;
                
                // 初始化
                xhr.open(config.method, config.url);
                
                // 发送
                xhr.send();

                // 监听成功响应
                xhr.onload = () => {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        resolve({
                            data: xhr.response,
                            status: xhr.status,
                            statusText: xhr.statusText,
                            headers: xhr.getAllResponseHeaders(),
                            config: config,
                            request: xhr
                        });
                    } else {
                        reject({
                            code: "ERR_BAD_REQUEST",
                            config,
                            message: "Request failed with status code " + xhr.status,
                            name: "AxiosError",
                            request: xhr
                        });
                    }
                }

                // 监听失败响应
                xhr.onerror = () => {
                    reject({
                        code: "ERR_NETWORK",
                        config,
                        message: "ERR_NETWORK",
                        name: "AxiosError",
                        request: xhr
                    });
                }
            });
        }

        // 核心类
        class Axios {
            constructor(instanceConfig) {
                this.defaults = instanceConfig;
            }

            /**
             * 发送 ajax 请求
             * @param {String|Object} configOrUrl url 或者请求配置对象
             * @param {?Object} config 请求配置对象
             * @returns {Promise}
             */
            request(configOrUrl = {}, config = {}) {
                // 判断第一个参数是否是 url
                if (typeof configOrUrl === 'string') {
                    config.url = configOrUrl;
                } else {
                    config = configOrUrl;
                }

                // 将传入的请求配置对象和全局请求配置对象合并
                config = Object.assign({}, this.defaults, config);

                // 设置默认请求方式是 GET
                config.method = (config.method || this.defaults.method || 'get').toUpperCase();
                
                // 调用函数发送请求
                return dispatchRequest.call(this, config);
            }
        }

        function bind(fn, thisArg) {
            return function wrap() {
                return fn.apply(thisArg, arguments);
            }
        }

        function createInstance(defaultConfig) {
            const context = new Axios(defaultConfig);
            const instance = bind(Axios.prototype.request, context);

            for (let key in context) {
                instance[key] = context[key];
            }

            Object.getOwnPropertyNames(Axios.prototype).forEach(key => {
                instance[key] = Axios.prototype[key].bind(context);
            });

            return instance;
        }

        // 创建 axios
        const axios = createInstance(defaults);

        // 测试请求
        console.log('=== 发送 GET 请求 ===');
        
        // 使用 GitHub API 测试
        axios({
            url: 'https://api.github.com/users',
            method: 'GET'
        })
        .then(response => {
            console.log('请求成功:', response);
            document.getElementById('result').innerHTML = `
                <h2>请求成功</h2>
                <p>状态码: ${response.status}</p>
                <p>数据长度: ${response.data.length} 个用户</p>
                <pre>${JSON.stringify(response.data[0], null, 2)}</pre>
            `;
        })
        .catch(error => {
            console.error('请求失败:', error);
            document.getElementById('result').innerHTML = `
                <h2>请求失败</h2>
                <p>错误信息: ${error.message}</p>
            `;
        });
    </script>
</body>
</html>

【代码注释】 dispatchRequest真正的请求执行函数 ------创建 XHR 对象、设置配置、监听事件、返回 Promise。request 方法负责配置合并与参数处理 ,然后调用 dispatchRequest。分离设计让代码职责清晰:request 处理配置,dispatchRequest 处理网络。市面应用 :这种"配置处理 + 执行分离"模式在 fetch-retryaxios-mock-adapter 等库中广泛使用,便于中间件插入。

【实战要点】

  • 经典应用场景:XHR 是浏览器最稳定的请求 API,支持进度监控、超时控制、取消请求,而 Fetch API 在旧浏览器不兼容
  • 常见坑 :忘记 xhr.send() 会导致请求不发送;xhr.onload 只在网络层成功时触发,HTTP 4xx/5xx 也算成功,需手动判断 status
  • 性能与最佳实践 :设置 responseType: 'json' 让浏览器自动解析 JSON,比 JSON.parse(xhr.responseText) 性能更好且更安全

【本章小结】

职责 函数 说明
配置合并 request Object.assign(defaults, config),method 转大写
真正发请求 dispatchRequest 返回 Promise,内部 new XHR
成功判定 xhr.onload 仅 2xx resolve,其余 reject 为 AxiosError

【面试考点】

Q4:dispatchRequest 为什么要单独拆出来?

A:拦截器只改 config,不应关心 XHR 细节;dispatchRequest 是链的「终点」,之后才可换适配器(xhr/http/fetch)。测试时也可 mock dispatchRequest 而不发真实网络请求。

Q5:requestdispatchRequest.call(this, config)this 有什么用?

A:后续若要在适配器里读 this.defaults 或挂实例级状态,需要绑定实例;与官方 dispatchRequest 作为纯函数略有不同,手写版保留 call(this) 便于扩展。


2.3 请求配置项深度解析

Axios 支持丰富的配置项,以下是核心配置的完整实现。

实战示例:完整配置项支持
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Axios 请求配置项完整示例</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        .config-item {
            background: #f5f5f5;
            padding: 10px;
            margin: 10px 0;
            border-radius: 4px;
        }
        button {
            padding: 10px 20px;
            margin: 10px 5px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <h1>Axios 请求配置项演示</h1>
    
    <div class="config-item">
        <h3>1. baseURL + url</h3>
        <button onclick="testBaseURL()">测试 baseURL</button>
        <div id="baseURL-result"></div>
    </div>

    <div class="config-item">
        <h3>2. params 参数</h3>
        <button onclick="testParams()">测试 params</button>
        <div id="params-result"></div>
    </div>

    <div class="config-item">
        <h3>3. headers 请求头</h3>
        <button onclick="testHeaders()">测试 headers</button>
        <div id="headers-result"></div>
    </div>

    <div class="config-item">
        <h3>4. data 请求体</h3>
        <button onclick="testData()">测试 data</button>
        <div id="data-result"></div>
    </div>

    <div class="config-item">
        <h3>5. responseType 响应类型</h3>
        <button onclick="testResponseType()">测试 responseType</button>
        <div id="responseType-result"></div>
    </div>

    <script type="module">
        // 定义默认请求配置项
        const defaults = {
            timeout: 0,
            responseType: 'json'
        };

        /**
         * 发送 ajax 请求
         */
        function dispatchRequest(config) {
            return new Promise((resolve, reject) => {
                const xhr = new XMLHttpRequest();
                xhr.responseType = config.responseType;
                
                // 初始化
                xhr.open(config.method, config.url);

                // 设置请求头
                if (config.headers) {
                    for (let key in config.headers) {
                        xhr.setRequestHeader(key, config.headers[key]);
                    }
                }

                // 设置请求体
                let body;
                // 只有允许的请求方法,才可以携带请求体
                if (['POST', 'PUT', 'PATCH'].includes(config.method)) {
                    if (typeof config.data === 'string') {
                        body = config.data;
                        // 如果没有显式设置 Content-Type,则设置默认值
                        if (!config.headers?.['Content-Type']) {
                            xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
                        }
                    } else if (Object.prototype.toString.call(config.data) === '[object Object]') {
                        body = JSON.stringify(config.data);
                        if (!config.headers?.['Content-Type']) {
                            xhr.setRequestHeader('Content-type', 'application/json');
                        }
                    } else {
                        body = config.data;
                    }
                }

                // 发送
                xhr.send(body);

                // 监听响应
                xhr.onload = () => {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        resolve({
                            data: xhr.response,
                            status: xhr.status,
                            statusText: xhr.statusText,
                            headers: xhr.getAllResponseHeaders(),
                            config: config,
                            request: xhr
                        });
                    } else {
                        reject({
                            code: "ERR_BAD_REQUEST",
                            config,
                            message: "Request failed with status code " + xhr.status,
                            name: "AxiosError",
                            request: xhr
                        });
                    }
                };

                xhr.onerror = () => {
                    reject({
                        code: "ERR_NETWORK",
                        config,
                        message: "ERR_NETWORK",
                        name: "AxiosError",
                        request: xhr
                    });
                };
            });
        }

        // 核心类
        class Axios {
            constructor(instanceConfig) {
                this.defaults = instanceConfig;
            }

            request(configOrUrl = {}, config = {}) {
                // 判断第一个参数是否是 url
                if (typeof configOrUrl === 'string') {
                    config.url = configOrUrl;
                } else {
                    config = configOrUrl;
                }

                // 合并配置项
                config = Object.assign({}, this.defaults, config);

                // 设置默认请求方式
                config.method = (config.method || this.defaults.method || 'get').toUpperCase();

                // 合并 baseURL 和 url
                if (config.baseURL && !config.url.startsWith('http://') && !config.url.startsWith('https://')) {
                    config.url = config.baseURL + config.url;
                }

                // 将 params 拼接到 url 后面
                if (config.params) {
                    const params = Object.entries(config.params)
                        .map(item => item[0] + '=' + encodeURIComponent(item[1]))
                        .join('&');
                    config.url += '?' + params;
                }
                
                return dispatchRequest.call(this, config);
            }

            get(url, config = {}) {
                return this.request(url, { ...config, method: 'GET' });
            }

            post(url, data, config = {}) {
                return this.request(url, { ...config, data, method: 'POST' });
            }
        }

        function bind(fn, thisArg) {
            return function wrap() {
                return fn.apply(thisArg, arguments);
            }
        }

        function createInstance(defaultConfig) {
            const context = new Axios(defaultConfig);
            const instance = bind(Axios.prototype.request, context);

            for (let key in context) {
                instance[key] = context[key];
            }

            Object.getOwnPropertyNames(Axios.prototype).forEach(key => {
                instance[key] = Axios.prototype[key].bind(context);
            });

            return instance;
        }

        // 创建 axios 并设置全局配置
        const axios = createInstance(defaults);

        // 全局配置示例
        axios.defaults.baseURL = 'https://api.github.com';
        axios.defaults.headers = {
            'Accept': 'application/vnd.github.v3+json'
        };

        // 暴露测试函数到全局
        window.testBaseURL = () => {
            axios({
                url: '/users',
                method: 'GET'
            })
            .then(response => {
                document.getElementById('baseURL-result').innerHTML = `
                    <p><strong>请求 URL:</strong> ${response.config.url}</p>
                    <p><strong>用户数量:</strong> ${response.data.length}</p>
                `;
            })
            .catch(error => {
                document.getElementById('baseURL-result').innerHTML = `
                    <p style="color: red;">错误: ${error.message}</p>
                `;
            });
        };

        window.testParams = () => {
            axios.get('/search/repositories', {
                params: {
                    q: 'javascript',
                    sort: 'stars'
                }
            })
            .then(response => {
                document.getElementById('params-result').innerHTML = `
                    <p><strong>请求 URL:</strong> ${response.config.url}</p>
                    <p><strong>仓库数量:</strong> ${response.data.total_count}</p>
                `;
            })
            .catch(error => {
                document.getElementById('params-result').innerHTML = `
                    <p style="color: red;">错误: ${error.message}</p>
                `;
            });
        };

        window.testHeaders = () => {
            axios.get('/users/github', {
                headers: {
                    'User-Agent': 'My-Axios-App/1.0'
                }
            })
            .then(response => {
                document.getElementById('headers-result').innerHTML = `
                    <p><strong>请求头:</strong></p>
                    <pre>${JSON.stringify(response.config.headers, null, 2)}</pre>
                    <p><strong>用户名:</strong> ${response.data.name}</p>
                `;
            })
            .catch(error => {
                document.getElementById('headers-result').innerHTML = `
                    <p style="color: red;">错误: ${error.message}</p>
                `;
            });
        };

        window.testData = () => {
            // 使用 JSONPlaceholder 测试 POST 请求
            const axios2 = createInstance({ responseType: 'json' });
            
            axios2.post('https://jsonplaceholder.typicode.com/posts', {
                title: 'foo',
                body: 'bar',
                userId: 1
            })
            .then(response => {
                document.getElementById('data-result').innerHTML = `
                    <p><strong>请求数据:</strong> {title: 'foo', body: 'bar', userId: 1}</p>
                    <p><strong>响应数据:</strong></p>
                    <pre>${JSON.stringify(response.data, null, 2)}</pre>
                `;
            })
            .catch(error => {
                document.getElementById('data-result').innerHTML = `
                    <p style="color: red;">错误: ${error.message}</p>
                `;
            });
        };

        window.testResponseType = () => {
            // 测试文本响应
            const axios3 = createInstance({ responseType: 'text' });
            
            axios3.get('https://api.github.com/zen')
            .then(response => {
                document.getElementById('responseType-result').innerHTML = `
                    <p><strong>响应类型:</strong> text</p>
                    <p><strong>Zen 格言:</strong> ${response.data}</p>
                `;
            })
            .catch(error => {
                document.getElementById('responseType-result').innerHTML = `
                    <p style="color: red;">错误: ${error.message}</p>
                `;
            });
        };
    </script>
</body>
</html>

【代码注释】 baseURL + url 合并采用前缀检测 ------只有相对路径(非 http://https:// 开头)才拼接,避免破坏完整 URL。params 处理使用 encodeURIComponent 编码参数值,防止特殊字符(如空格、&)破坏 URL 结构。data 处理根据类型自动设置 Content-Type------对象转 JSON、字符串直接发送。市面应用 :REST API 调用几乎都用 baseURL(如 https://api.example.com/v1)+ 相对路径,方便环境切换;params 用于 GET 请求的查询参数构建。

【实战要点】

  • 经典应用场景baseURL 用于环境管理 (开发/测试/生产 API 地址切换);params 用于列表筛选搜索headers 用于身份认证Authorization: Bearer token
  • 常见坑
    1. params 值未编码导致 URL 解析错误(如 {q: 'a&b'} 变成 ?q=a&bb 被当成独立参数)
    2. POST 发送对象时忘设置 Content-Type: application/json,后端无法解析
    3. baseURL 末尾斜杠不一致(https://api.com + /v1/users vs https://api.com/ + v1/users)导致双斜杠或缺少斜杠
  • 性能与最佳实践
    1. 全局配置用 axios.defaults.baseURL,避免每次重复设置
    2. 敏感信息(如 token)不要放在 URL(params),应放 headers 避免日志泄露
    3. 大文件上传用 FormData,不要手动转 JSON

【本章小结】

配置项 作用 常见值
baseURL 基础 URL,自动拼接相对路径 https://api.example.com
url 请求路径 /usershttps://other.com/api
params URL 查询参数 {page: 1, size: 10}
headers 请求头 {'Authorization': 'Bearer token'}
data 请求体(POST/PUT/PATCH) {name: 'John'}FormData
method 请求方法 'GET''POST''PUT''DELETE'
responseType 响应数据类型 'json''text''blob''arraybuffer'

记忆口诀"base 接路径,params 查询配;headers 身份认,data 请求体;method 方法定,response 类型归"

【面试考点】

Q2:axios 如何实现跨环境的 baseURL 处理?

A:axios 的 baseURL 只对相对路径 生效,检测逻辑是 !url.startsWith('http://') && !url.startsWith('https://')。这样设计让完整 URL(如第三方 API)不受 baseURL 影响。追问"如何处理不同环境的 baseURL"时答:通过构建工具(如 Webpack 的 DefinePlugin)注入环境变量,axios.defaults.baseURL = process.env.API_BASE_URL

Q3:为什么 POST 请求的 data 要根据类型设置不同的 Content-Type?

A:因为 HTTP 协议规定请求体格式必须与 Content-Type 匹配 ,后端才能正确解析。对象转 JSON 需设置 application/json,字符串如 a=1&b=2application/x-www-form-urlencoded,文件上传用 multipart/form-data。axios 能自动识别对象和字符串,但 FormData 需手动设置。


2.4 响应结果处理机制

Axios 的响应对象结构标准化,便于统一处理。
#mermaid-svg-xjGDfo1hVwa3Snji{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xjGDfo1hVwa3Snji .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xjGDfo1hVwa3Snji .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xjGDfo1hVwa3Snji .error-icon{fill:#552222;}#mermaid-svg-xjGDfo1hVwa3Snji .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xjGDfo1hVwa3Snji .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xjGDfo1hVwa3Snji .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xjGDfo1hVwa3Snji .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xjGDfo1hVwa3Snji .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xjGDfo1hVwa3Snji .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xjGDfo1hVwa3Snji .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xjGDfo1hVwa3Snji .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xjGDfo1hVwa3Snji .marker.cross{stroke:#333333;}#mermaid-svg-xjGDfo1hVwa3Snji svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xjGDfo1hVwa3Snji p{margin:0;}#mermaid-svg-xjGDfo1hVwa3Snji .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xjGDfo1hVwa3Snji .cluster-label text{fill:#333;}#mermaid-svg-xjGDfo1hVwa3Snji .cluster-label span{color:#333;}#mermaid-svg-xjGDfo1hVwa3Snji .cluster-label span p{background-color:transparent;}#mermaid-svg-xjGDfo1hVwa3Snji .label text,#mermaid-svg-xjGDfo1hVwa3Snji span{fill:#333;color:#333;}#mermaid-svg-xjGDfo1hVwa3Snji .node rect,#mermaid-svg-xjGDfo1hVwa3Snji .node circle,#mermaid-svg-xjGDfo1hVwa3Snji .node ellipse,#mermaid-svg-xjGDfo1hVwa3Snji .node polygon,#mermaid-svg-xjGDfo1hVwa3Snji .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xjGDfo1hVwa3Snji .rough-node .label text,#mermaid-svg-xjGDfo1hVwa3Snji .node .label text,#mermaid-svg-xjGDfo1hVwa3Snji .image-shape .label,#mermaid-svg-xjGDfo1hVwa3Snji .icon-shape .label{text-anchor:middle;}#mermaid-svg-xjGDfo1hVwa3Snji .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xjGDfo1hVwa3Snji .rough-node .label,#mermaid-svg-xjGDfo1hVwa3Snji .node .label,#mermaid-svg-xjGDfo1hVwa3Snji .image-shape .label,#mermaid-svg-xjGDfo1hVwa3Snji .icon-shape .label{text-align:center;}#mermaid-svg-xjGDfo1hVwa3Snji .node.clickable{cursor:pointer;}#mermaid-svg-xjGDfo1hVwa3Snji .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xjGDfo1hVwa3Snji .arrowheadPath{fill:#333333;}#mermaid-svg-xjGDfo1hVwa3Snji .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xjGDfo1hVwa3Snji .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xjGDfo1hVwa3Snji .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xjGDfo1hVwa3Snji .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xjGDfo1hVwa3Snji .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xjGDfo1hVwa3Snji .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xjGDfo1hVwa3Snji .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xjGDfo1hVwa3Snji .cluster text{fill:#333;}#mermaid-svg-xjGDfo1hVwa3Snji .cluster span{color:#333;}#mermaid-svg-xjGDfo1hVwa3Snji div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xjGDfo1hVwa3Snji .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xjGDfo1hVwa3Snji rect.text{fill:none;stroke-width:0;}#mermaid-svg-xjGDfo1hVwa3Snji .icon-shape,#mermaid-svg-xjGDfo1hVwa3Snji .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xjGDfo1hVwa3Snji .icon-shape p,#mermaid-svg-xjGDfo1hVwa3Snji .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xjGDfo1hVwa3Snji .icon-shape .label rect,#mermaid-svg-xjGDfo1hVwa3Snji .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xjGDfo1hVwa3Snji .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xjGDfo1hVwa3Snji .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xjGDfo1hVwa3Snji :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 200-299
其他
XHR 响应
状态码判断
构建成功响应
构建错误响应
resolve response
reject error

【代码注释】 此流程图说明响应处理的核心逻辑------根据 HTTP 状态码决定 Promise 的走向。200-299 范围的状态码被视为成功,resolve 响应对象;其他状态码(4xx、5xx)被视为失败,reject 错误对象。关键点:xhr.onload 在所有 HTTP 请求完成后触发,包括 404、500,因此必须手动判断 status。这与 xhr.onerror 不同------后者只在网络层失败时触发。

实战示例:响应处理完整实现
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Axios 响应处理机制</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 1000px;
            margin: 50px auto;
            padding: 20px;
        }
        .response-box {
            background: #f5f5f5;
            padding: 15px;
            margin: 15px 0;
            border-radius: 5px;
            border-left: 4px solid #4CAF50;
        }
        .error-box {
            background: #ffebee;
            padding: 15px;
            margin: 15px 0;
            border-radius: 5px;
            border-left: 4px solid #f44336;
        }
        button {
            padding: 10px 20px;
            margin: 5px;
            cursor: pointer;
            background: #2196F3;
            color: white;
            border: none;
            border-radius: 4px;
        }
        button:hover {
            background: #0b7dda;
        }
        pre {
            background: white;
            padding: 10px;
            border-radius: 4px;
            overflow-x: auto;
        }
    </style>
</head>
<body>
    <h1>Axios 响应处理机制演示</h1>
    
    <div>
        <button onclick="testSuccess()">测试成功响应 (200)</button>
        <button onclick="testNotFound()">测试 404 错误</button>
        <button onclick="testNetworkError()">测试网络错误</button>
    </div>

    <div id="result"></div>

    <script type="module">
        const defaults = {
            timeout: 5000,
            responseType: 'json'
        };

        function dispatchRequest(config) {
            return new Promise((resolve, reject) => {
                const xhr = new XMLHttpRequest();
                xhr.responseType = config.responseType;
                xhr.timeout = config.timeout;
                
                xhr.open(config.method, config.url);

                if (config.headers) {
                    for (let key in config.headers) {
                        xhr.setRequestHeader(key, config.headers[key]);
                    }
                }

                let body;
                if (['POST', 'PUT', 'PATCH'].includes(config.method)) {
                    if (typeof config.data === 'string') {
                        body = config.data;
                    } else if (Object.prototype.toString.call(config.data) === '[object Object]') {
                        body = JSON.stringify(config.data);
                        if (!config.headers?.['Content-Type']) {
                            xhr.setRequestHeader('Content-type', 'application/json');
                        }
                    } else {
                        body = config.data;
                    }
                }

                xhr.send(body);

                // 监听成功响应
                xhr.onload = () => {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        resolve({
                            // 数据
                            data: xhr.response,
                            // 状态码
                            status: xhr.status,
                            // 状态文本
                            statusText: xhr.statusText,
                            // 响应头(原始字符串)
                            headers: xhr.getAllResponseHeaders(),
                            // 响应头(解析为对象)
                            headersObj: parseHeaders(xhr.getAllResponseHeaders()),
                            // 请求配置
                            config: config,
                            // XHR 对象
                            request: xhr
                        });
                    } else {
                        // HTTP 错误(4xx, 5xx)
                        reject({
                            code: "ERR_BAD_REQUEST",
                            config,
                            message: `Request failed with status code ${xhr.status}`,
                            name: "AxiosError",
                            request: xhr,
                            response: {
                                data: xhr.response,
                                status: xhr.status,
                                statusText: xhr.statusText,
                                headers: parseHeaders(xhr.getAllResponseHeaders())
                            }
                        });
                    }
                };

                // 监听网络错误
                xhr.onerror = () => {
                    reject({
                        code: "ERR_NETWORK",
                        config,
                        message: "Network Error",
                        name: "AxiosError",
                        request: xhr
                    });
                };

                // 监听超时
                xhr.ontimeout = () => {
                    reject({
                        code: "ECONNABORTED",
                        config,
                        message: `timeout of ${config.timeout}ms exceeded`,
                        name: "AxiosError",
                        request: xhr
                    });
                };
            });
        }

        // 解析响应头为对象
        function parseHeaders(headersStr) {
            if (!headersStr) return {};
            
            return headersStr
                .split('\r\n')
                .filter(line => line.trim())
                .reduce((obj, line) => {
                    const [key, ...valueParts] = line.split(': ');
                    const value = valueParts.join(': '); // 处理值中包含 ": " 的情况
                    if (key) {
                        obj[key] = value;
                    }
                    return obj;
                }, {});
        }

        // Axios 类省略,直接使用简化版本
        class Axios {
            constructor(config) {
                this.defaults = config;
            }

            request(urlOrConfig, config = {}) {
                if (typeof urlOrConfig === 'string') {
                    config.url = urlOrConfig;
                } else {
                    config = urlOrConfig;
                }

                config = Object.assign({}, this.defaults, config);
                config.method = (config.method || 'get').toUpperCase();

                return dispatchRequest(config);
            }

            get(url, config = {}) {
                return this.request(url, { ...config, method: 'GET' });
            }

            post(url, data, config = {}) {
                return this.request(url, { ...config, data, method: 'POST' });
            }
        }

        function createInstance(defaultConfig) {
            const context = new Axios(defaultConfig);
            const instance = function(config) {
                return context.request(config);
            };

            instance.defaults = context.defaults;
            ['get', 'post', 'request'].forEach(method => {
                instance[method] = context[method].bind(context);
            });

            return instance;
        }

        const axios = createInstance(defaults);

        // 测试函数
        window.testSuccess = () => {
            axios.get('https://api.github.com/users/github')
                .then(response => {
                    displayResponse('成功响应示例', response, false);
                })
                .catch(error => {
                    displayError(error);
                });
        };

        window.testNotFound = () => {
            axios.get('https://api.github.com/users/this-user-definitely-does-not-exist-12345')
                .then(response => {
                    displayResponse('响应', response, false);
                })
                .catch(error => {
                    displayResponse('404 错误响应(注意:HTTP错误也返回response)', error, true);
                });
        };

        window.testNetworkError = () => {
            axios.get('https://this-domain-definitely-does-not-exist-12345.com')
                .then(response => {
                    displayResponse('响应', response, false);
                })
                .catch(error => {
                    displayError(error);
                });
        };

        function displayResponse(title, data, isError = false) {
            const boxClass = isError ? 'error-box' : 'response-box';
            const html = `
                <div class="${boxClass}">
                    <h3>${title}</h3>
                    <p><strong>状态码:</strong> ${data.status || 'N/A'}</p>
                    <p><strong>状态文本:</strong> ${data.statusText || 'N/A'}</p>
                    ${data.code ? `<p><strong>错误码:</strong> ${data.code}</p>` : ''}
                    <p><strong>消息:</strong> ${data.message || 'Success'}</p>
                    ${data.data ? `<p><strong>响应数据:</strong></p><pre>${JSON.stringify(data.data, null, 2)}</pre>` : ''}
                    ${data.headersObj ? `<p><strong>响应头:</strong></p><pre>${JSON.stringify(data.headersObj, null, 2)}</pre>` : ''}
                    ${data.config ? `<p><strong>请求配置:</strong></p><pre>${JSON.stringify({url: data.config.url, method: data.config.method}, null, 2)}</pre>` : ''}
                </div>
            `;
            document.getElementById('result').innerHTML = html;
        }

        function displayError(error) {
            const html = `
                <div class="error-box">
                    <h3>网络错误</h3>
                    <p><strong>错误码:</strong> ${error.code}</p>
                    <p><strong>错误消息:</strong> ${error.message}</p>
                    <p><strong>错误名称:</strong> ${error.name}</p>
                    <p><strong>请求配置:</strong></p>
                    <pre>${JSON.stringify({url: error.config?.url, method: error.config?.method}, null, 2)}</pre>
                </div>
            `;
            document.getElementById('result').innerHTML = html;
        }
    </script>
</body>
</html>

【代码注释】 响应处理的核心是区分 HTTP 错误与网络错误 ------xhr.onload 会在 HTTP 请求完成时触发(包括 404、500),需手动判断 status 范围;xhr.onerror 只在网络层失败(如 DNS 解析失败、跨域被阻止)时触发。parseHeaders 函数处理多行响应头解析 ,用 split('\r\n') 分割后逐行提取 key: value市面应用:这种响应结构在所有 HTTP 客户端中通用(Fetch、Request、SuperAgent),便于错误处理中间件复用。

【实战要点】

  • 经典应用场景
    1. 统一错误处理 :根据 error.code 区分错误类型,ERR_NETWORK 提示"网络连接失败",ERR_BAD_REQUEST 提示"服务器返回错误"
    2. 响应日志记录response.config.url + response.status 构成请求日志,用于调试
    3. 响应拦截器 :统一提取 response.data,简化业务代码
  • 常见坑
    1. 误以为 xhr.onerror 会捕获 4xx/5xx,其实这些在 xhr.onload
    2. 忘记 responseType: 'json' 导致 response.data 是字符串而非对象
    3. 跨域请求时,xhr.status 为 0 且 xhr.onerror 触发(CORS 被阻止)
  • 性能与最佳实践
    1. response.data 直接获取解析后数据,避免重复 JSON.parse
    2. 响应拦截器中统一处理 error.response,避免每个请求单独写 catch

【本章小结】

事件 何时触发 业务含义
xhr.onload HTTP 完成(含 404/500) status 区分成功/失败
xhr.onerror 网络层失败、CORS 拦截 status 常为 0
xhr.ontimeout 超过 xhr.timeout ECONNABORTED

响应对象建议统一:{ data, status, statusText, headers, config, request },与官方 AxiosResponse 对齐,方便拦截器只返回 response.data

【面试考点】

Q6:为什么 404 不会进 xhr.onerror

A:onerror 表示传输层 失败;404 是服务器正常返回的 HTTP 语义错误,状态码在 onload 里可读。axios 在 onload 里对非 2xx reject,才能被 catch 或响应拦截器的错误分支捕获。

Q7:跨域失败时 status 为什么是 0?

A:浏览器安全策略下,JS 读不到真实状态码,XHR 以失败结束,常表现为 onerror + status === 0。这与 CORS 响应头未配置有关,不是 axios 特例。


2.5 超时设置与错误处理

超时机制防止请求无限等待,提升用户体验。
#mermaid-svg-sf5krCk4LsYq1Z8R{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sf5krCk4LsYq1Z8R .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sf5krCk4LsYq1Z8R .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sf5krCk4LsYq1Z8R .error-icon{fill:#552222;}#mermaid-svg-sf5krCk4LsYq1Z8R .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sf5krCk4LsYq1Z8R .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sf5krCk4LsYq1Z8R .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sf5krCk4LsYq1Z8R .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sf5krCk4LsYq1Z8R .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sf5krCk4LsYq1Z8R .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sf5krCk4LsYq1Z8R .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sf5krCk4LsYq1Z8R .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sf5krCk4LsYq1Z8R .marker.cross{stroke:#333333;}#mermaid-svg-sf5krCk4LsYq1Z8R svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sf5krCk4LsYq1Z8R p{margin:0;}#mermaid-svg-sf5krCk4LsYq1Z8R .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sf5krCk4LsYq1Z8R .cluster-label text{fill:#333;}#mermaid-svg-sf5krCk4LsYq1Z8R .cluster-label span{color:#333;}#mermaid-svg-sf5krCk4LsYq1Z8R .cluster-label span p{background-color:transparent;}#mermaid-svg-sf5krCk4LsYq1Z8R .label text,#mermaid-svg-sf5krCk4LsYq1Z8R span{fill:#333;color:#333;}#mermaid-svg-sf5krCk4LsYq1Z8R .node rect,#mermaid-svg-sf5krCk4LsYq1Z8R .node circle,#mermaid-svg-sf5krCk4LsYq1Z8R .node ellipse,#mermaid-svg-sf5krCk4LsYq1Z8R .node polygon,#mermaid-svg-sf5krCk4LsYq1Z8R .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sf5krCk4LsYq1Z8R .rough-node .label text,#mermaid-svg-sf5krCk4LsYq1Z8R .node .label text,#mermaid-svg-sf5krCk4LsYq1Z8R .image-shape .label,#mermaid-svg-sf5krCk4LsYq1Z8R .icon-shape .label{text-anchor:middle;}#mermaid-svg-sf5krCk4LsYq1Z8R .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sf5krCk4LsYq1Z8R .rough-node .label,#mermaid-svg-sf5krCk4LsYq1Z8R .node .label,#mermaid-svg-sf5krCk4LsYq1Z8R .image-shape .label,#mermaid-svg-sf5krCk4LsYq1Z8R .icon-shape .label{text-align:center;}#mermaid-svg-sf5krCk4LsYq1Z8R .node.clickable{cursor:pointer;}#mermaid-svg-sf5krCk4LsYq1Z8R .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sf5krCk4LsYq1Z8R .arrowheadPath{fill:#333333;}#mermaid-svg-sf5krCk4LsYq1Z8R .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sf5krCk4LsYq1Z8R .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sf5krCk4LsYq1Z8R .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sf5krCk4LsYq1Z8R .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sf5krCk4LsYq1Z8R .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sf5krCk4LsYq1Z8R .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sf5krCk4LsYq1Z8R .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sf5krCk4LsYq1Z8R .cluster text{fill:#333;}#mermaid-svg-sf5krCk4LsYq1Z8R .cluster span{color:#333;}#mermaid-svg-sf5krCk4LsYq1Z8R div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sf5krCk4LsYq1Z8R .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sf5krCk4LsYq1Z8R rect.text{fill:none;stroke-width:0;}#mermaid-svg-sf5krCk4LsYq1Z8R .icon-shape,#mermaid-svg-sf5krCk4LsYq1Z8R .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sf5krCk4LsYq1Z8R .icon-shape p,#mermaid-svg-sf5krCk4LsYq1Z8R .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sf5krCk4LsYq1Z8R .icon-shape .label rect,#mermaid-svg-sf5krCk4LsYq1Z8R .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sf5krCk4LsYq1Z8R .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sf5krCk4LsYq1Z8R .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sf5krCk4LsYq1Z8R :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} < timeout
>= timeout
发起请求
设置 xhr.timeout
响应时间
触发 xhr.ontimeout
正常响应
reject ECONNABORTED
正常处理

【代码注释】 此图展示超时机制的决策流程------当响应时间小于设定值时走正常响应路径,否则触发 xhr.ontimeout 事件并 reject 错误对象(错误码 ECONNABORTED)。重要概念:超时是客户端中止等待,请求可能已到达服务器并正在处理,因此超时后不应自动重试写操作(POST/DELETE),避免数据重复提交。

实战示例:超时与完整错误处理
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Axios 超时与错误处理</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 900px;
            margin: 50px auto;
            padding: 20px;
        }
        .test-section {
            background: #f9f9f9;
            padding: 20px;
            margin: 20px 0;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        button {
            padding: 10px 20px;
            margin: 5px;
            cursor: pointer;
            background: #2196F3;
            color: white;
            border: none;
            border-radius: 4px;
        }
        button:hover {
            background: #0b7dda;
        }
        .result {
            margin-top: 15px;
            padding: 15px;
            border-radius: 4px;
            font-family: monospace;
            white-space: pre-wrap;
        }
        .success {
            background: #d4edda;
            color: #155724;
            border-left: 4px solid #28a745;
        }
        .error {
            background: #f8d7da;
            color: #721c24;
            border-left: 4px solid #dc3545;
        }
        .timeout {
            background: #fff3cd;
            color: #856404;
            border-left: 4px solid #ffc107;
        }
    </style>
</head>
<body>
    <h1>Axios 超时与错误处理演示</h1>

    <div class="test-section">
        <h2>1. 超时测试</h2>
        <p>使用一个会延迟响应的 API(delay.ms)测试超时机制</p>
        <button onclick="testTimeout()">测试 2 秒超时</button>
        <button onclick="testNoTimeout()">测试不超时</button>
        <div id="timeout-result" class="result"></div>
    </div>

    <div class="test-section">
        <h2>2. 错误类型测试</h2>
        <button onclick="testNetworkError()">测试网络错误</button>
        <button onclick="testHTTPError()">测试 HTTP 错误 (404)</button>
        <button onclick="testServerError()">测试服务器错误 (500)</button>
        <div id="error-result" class="result"></div>
    </div>

    <div class="test-section">
        <h2>3. 全局错误处理示例</h2>
        <button onclick="testGlobalHandler()">测试全局拦截器</button>
        <div id="global-result" class="result"></div>
    </div>

    <script type="module">
        const defaults = {
            timeout: 0,  // 默认不超时
            responseType: 'json'
        };

        function dispatchRequest(config) {
            return new Promise((resolve, reject) => {
                const xhr = new XMLHttpRequest();
                xhr.responseType = config.responseType;
                xhr.timeout = config.timeout;  // 设置超时时间
                
                xhr.open(config.method, config.url);

                if (config.headers) {
                    for (let key in config.headers) {
                        xhr.setRequestHeader(key, config.headers[key]);
                    }
                }

                let body;
                if (['POST', 'PUT', 'PATCH'].includes(config.method)) {
                    if (typeof config.data === 'string') {
                        body = config.data;
                    } else if (Object.prototype.toString.call(config.data) === '[object Object]') {
                        body = JSON.stringify(config.data);
                        if (!config.headers?.['Content-Type']) {
                            xhr.setRequestHeader('Content-type', 'application/json');
                        }
                    } else {
                        body = config.data;
                    }
                }

                xhr.send(body);

                xhr.onload = () => {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        resolve({
                            data: xhr.response,
                            status: xhr.status,
                            statusText: xhr.statusText,
                            config: config,
                            request: xhr
                        });
                    } else {
                        reject({
                            code: "ERR_BAD_REQUEST",
                            config,
                            message: `Request failed with status code ${xhr.status}`,
                            name: "AxiosError",
                            request: xhr,
                            response: {
                                data: xhr.response,
                                status: xhr.status,
                                statusText: xhr.statusText
                            }
                        });
                    }
                };

                xhr.onerror = () => {
                    reject({
                        code: "ERR_NETWORK",
                        config,
                        message: "Network Error",
                        name: "AxiosError",
                        request: xhr
                    });
                };

                // 超时事件
                xhr.ontimeout = () => {
                    reject({
                        code: "ECONNABORTED",
                        config,
                        message: `timeout of ${config.timeout}ms exceeded`,
                        name: "AxiosError",
                        request: xhr
                    });
                };
            });
        }

        class Axios {
            constructor(config) {
                this.defaults = config;
            }

            request(urlOrConfig, config = {}) {
                if (typeof urlOrConfig === 'string') {
                    config.url = urlOrConfig;
                } else {
                    config = urlOrConfig;
                }

                config = Object.assign({}, this.defaults, config);
                config.method = (config.method || 'get').toUpperCase();

                return dispatchRequest(config);
            }

            get(url, config = {}) {
                return this.request(url, { ...config, method: 'GET' });
            }

            post(url, data, config = {}) {
                return this.request(url, { ...config, data, method: 'POST' });
            }
        }

        function createInstance(defaultConfig) {
            const context = new Axios(defaultConfig);
            const instance = function(config) {
                return context.request(config);
            };

            instance.defaults = context.defaults;
            ['get', 'post', 'request'].forEach(method => {
                instance[method] = context[method].bind(context);
            });

            return instance;
        }

        // 创建两个实例:一个带超时,一个不带
        const axiosWithTimeout = createInstance({ ...defaults, timeout: 2000 });
        const axiosNoTimeout = createInstance(defaults);

        // 测试函数
        window.testTimeout = () => {
            const resultDiv = document.getElementById('timeout-result');
            resultDiv.textContent = '发送请求...(2秒超时)';
            resultDiv.className = 'result';

            // 使用 delay.ms 模拟延迟响应
            axiosWithTimeout.get('https://delay.ms/3/https://api.github.com/users/github')
                .then(response => {
                    resultDiv.textContent = `成功:${response.data.name}`;
                    resultDiv.className = 'result success';
                })
                .catch(error => {
                    if (error.code === 'ECONNABORTED') {
                        resultDiv.textContent = `超时错误:${error.message}`;
                        resultDiv.className = 'result timeout';
                    } else {
                        resultDiv.textContent = `其他错误:${error.message}`;
                        resultDiv.className = 'result error';
                    }
                });
        };

        window.testNoTimeout = () => {
            const resultDiv = document.getElementById('timeout-result');
            resultDiv.textContent = '发送请求...(不超时)';
            resultDiv.className = 'result';

            axiosNoTimeout.get('https://delay.ms/1/https://api.github.com/users/github')
                .then(response => {
                    resultDiv.textContent = `成功:${response.data.name}`;
                    resultDiv.className = 'result success';
                })
                .catch(error => {
                    resultDiv.textContent = `错误:${error.message} (${error.code})`;
                    resultDiv.className = 'result error';
                });
        };

        window.testNetworkError = () => {
            const resultDiv = document.getElementById('error-result');
            resultDiv.textContent = '发送网络错误请求...';
            resultDiv.className = 'result';

            axiosNoTimeout.get('https://this-domain-does-not-exist-12345.com/api')
                .then(response => {
                    resultDiv.textContent = `成功:${JSON.stringify(response.data)}`;
                    resultDiv.className = 'result success';
                })
                .catch(error => {
                    let errorMsg = `错误码: ${error.code}\n`;
                    errorMsg += `错误消息: ${error.message}\n`;
                    errorMsg += `错误名称: ${error.name}`;
                    
                    resultDiv.textContent = errorMsg;
                    resultDiv.className = 'result error';
                });
        };

        window.testHTTPError = () => {
            const resultDiv = document.getElementById('error-result');
            resultDiv.textContent = '发送 404 请求...';
            resultDiv.className = 'result';

            axiosNoTimeout.get('https://api.github.com/users/this-user-does-not-exist-12345')
                .then(response => {
                    resultDiv.textContent = `成功:${JSON.stringify(response.data)}`;
                    resultDiv.className = 'result success';
                })
                .catch(error => {
                    let errorMsg = `错误码: ${error.code}\n`;
                    errorMsg += `错误消息: ${error.message}\n`;
                    errorMsg += `状态码: ${error.response?.status}\n`;
                    errorMsg += `错误名称: ${error.name}`;
                    
                    resultDiv.textContent = errorMsg;
                    resultDiv.className = 'result error';
                });
        };

        window.testServerError = () => {
            const resultDiv = document.getElementById('error-result');
            resultDiv.textContent = '发送 500 错误请求...';
            resultDiv.className = 'result';

            // 使用 httpbin.org 测试服务器错误
            axiosNoTimeout.get('https://httpbin.org/status/500')
                .then(response => {
                    resultDiv.textContent = `成功:状态码 ${response.status}`;
                    resultDiv.className = 'result success';
                })
                .catch(error => {
                    let errorMsg = `错误码: ${error.code}\n`;
                    errorMsg += `错误消息: ${error.message}\n`;
                    errorMsg += `状态码: ${error.response?.status}\n`;
                    errorMsg += `错误名称: ${error.name}`;
                    
                    resultDiv.textContent = errorMsg;
                    resultDiv.className = 'result error';
                });
        };

        window.testGlobalHandler = () => {
            const resultDiv = document.getElementById('global-result');
            resultDiv.textContent = '测试全局拦截器错误处理...';
            resultDiv.className = 'result';

            // 模拟全局错误处理
            const axios = createInstance(defaults);
            
            // 模拟请求拦截器
            const requestInterceptor = config => {
                console.log('发送请求:', config.url);
                return config;
            };

            // 模拟响应拦截器
            const responseInterceptor = response => {
                console.log('收到响应:', response.status);
                return response;
            };

            // 模拟错误拦截器
            const errorInterceptor = error => {
                if (error.code === 'ERR_NETWORK') {
                    return Promise.reject({
                        ...error,
                        userMessage: '网络连接失败,请检查网络设置'
                    });
                } else if (error.code === 'ERR_BAD_REQUEST') {
                    return Promise.reject({
                        ...error,
                        userMessage: `请求失败,状态码:${error.response?.status}`
                    });
                } else if (error.code === 'ECONNABORTED') {
                    return Promise.reject({
                        ...error,
                        userMessage: '请求超时,请稍后重试'
                    });
                }
                return Promise.reject(error);
            };

            // 使用拦截器(简化实现)
            const requestWithInterceptors = config => {
                config = requestInterceptor(config);
                return axios.request(config)
                    .then(responseInterceptor)
                    .catch(errorInterceptor);
            };

            requestWithInterceptors({ url: 'https://nonexistent-domain-12345.com' })
                .then(response => {
                    resultDiv.textContent = `成功:${response.data}`;
                    resultDiv.className = 'result success';
                })
                .catch(error => {
                    resultDiv.textContent = `用户友好错误消息:${error.userMessage || error.message}`;
                    resultDiv.className = 'result error';
                });
        };
    </script>
</body>
</html>

【代码注释】 超时设置通过 xhr.timeout 实现,单位是毫秒 。超时触发 xhr.ontimeout 事件,此时 xhr.status 为 0 且 xhr.readyState 为 4(请求完成但被中止)。错误码 ECONNABORTED 是 axios 的约定,表示连接被中止(包括超时和取消)。市面应用 :移动端网络不稳定,超时设置通常比桌面端短(3-5秒);长轮询场景禁用超时(timeout: 0)。

【实战要点】

  • 经典应用场景
    1. 移动端 API :设置 timeout: 5000,5秒未响应则提示用户"网络较弱"
    2. 文件上传 :大文件上传禁用超时或设置较长值(如 timeout: 60000
    3. 轮询请求 :短超时避免阻塞下一轮(如 timeout: 3000
  • 常见坑
    1. 超时后请求已发送到服务器,可能服务端仍处理成功(超时 ≠ 取消
    2. timeout: 0 表示永不超时,可能导致请求永久挂起
    3. 超时错误 ECONNABORTED 也用于请求取消,需用 axios.isCancel() 区分
  • 性能与最佳实践
    1. 根据网络环境调整超时:移动端 3-5秒,桌面端 10-30秒
    2. 全局配置 axios.defaults.timeout = 10000,特殊请求单独覆盖
    3. 超时后提供重试按钮,不要自动重试(可能重复提交订单等)

【本章小结】

错误码 含义 触发场景 处理建议
ERR_NETWORK 网络错误 DNS 失败、跨域、连接被拒绝 检查网络、提示用户
ERR_BAD_REQUEST HTTP 错误 4xx(客户端错误)、5xx(服务器错误) 显示具体错误信息
ECONNABORTED 连接中止 超时、主动取消 提示超时或取消

记忆口诀"Network 网络错,BadRequest HTTP 错,Abort 超时取消要"

【面试考点】

Q4:axios 的超时机制是如何实现的?超时后请求会被取消吗?

A:通过 xhr.timeout = ms 设置超时时间,超时触发 xhr.ontimeout 事件。关键点 :超时只是客户端中止等待,请求可能已到达服务器并正在处理。如果服务端处理成功,会产生数据不一致(如重复扣款)。追问"如何避免"时答:超时后不要自动重试写操作(POST/DELETE),可提供重试按钮让用户手动触发;读操作(GET)可安全重试。


2.6 请求取消机制

Axios 的取消请求基于 Promise 状态控制实现。
XHR CancelToken Client XHR CancelToken Client #mermaid-svg-x8rvARNzdkp9uoXE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-x8rvARNzdkp9uoXE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-x8rvARNzdkp9uoXE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-x8rvARNzdkp9uoXE .error-icon{fill:#552222;}#mermaid-svg-x8rvARNzdkp9uoXE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-x8rvARNzdkp9uoXE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-x8rvARNzdkp9uoXE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-x8rvARNzdkp9uoXE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-x8rvARNzdkp9uoXE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-x8rvARNzdkp9uoXE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-x8rvARNzdkp9uoXE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-x8rvARNzdkp9uoXE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-x8rvARNzdkp9uoXE .marker.cross{stroke:#333333;}#mermaid-svg-x8rvARNzdkp9uoXE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-x8rvARNzdkp9uoXE p{margin:0;}#mermaid-svg-x8rvARNzdkp9uoXE .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-x8rvARNzdkp9uoXE text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-x8rvARNzdkp9uoXE .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-x8rvARNzdkp9uoXE .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-x8rvARNzdkp9uoXE .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-x8rvARNzdkp9uoXE .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-x8rvARNzdkp9uoXE #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-x8rvARNzdkp9uoXE .sequenceNumber{fill:white;}#mermaid-svg-x8rvARNzdkp9uoXE #sequencenumber{fill:#333;}#mermaid-svg-x8rvARNzdkp9uoXE #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-x8rvARNzdkp9uoXE .messageText{fill:#333;stroke:none;}#mermaid-svg-x8rvARNzdkp9uoXE .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-x8rvARNzdkp9uoXE .labelText,#mermaid-svg-x8rvARNzdkp9uoXE .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-x8rvARNzdkp9uoXE .loopText,#mermaid-svg-x8rvARNzdkp9uoXE .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-x8rvARNzdkp9uoXE .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-x8rvARNzdkp9uoXE .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-x8rvARNzdkp9uoXE .noteText,#mermaid-svg-x8rvARNzdkp9uoXE .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-x8rvARNzdkp9uoXE .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-x8rvARNzdkp9uoXE .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-x8rvARNzdkp9uoXE .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-x8rvARNzdkp9uoXE .actorPopupMenu{position:absolute;}#mermaid-svg-x8rvARNzdkp9uoXE .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-x8rvARNzdkp9uoXE .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-x8rvARNzdkp9uoXE .actor-man circle,#mermaid-svg-x8rvARNzdkp9uoXE line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-x8rvARNzdkp9uoXE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求进行中 创建 cancelToken 发送请求(config含cancelToken) then(取消函数) 调用 cancel() Promise resolve abort() 取消请求 onabort 触发

【代码注释】 此序列图展示取消请求的完整流程------CancelToken 内部维护一个 Promise,调用 cancel() 时 resolve 该 Promise,XHR 监听到 resolve 后调用 abort() 方法取消请求。核心机制是异步通知dispatchRequest 中用 config.cancelToken.then(() => xhr.abort()) 实现取消监听。取消后 xhr.onabort 触发,reject 错误对象(code: "ERR_CANCELED")。

实战示例:请求取消实现
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Axios 请求取消机制</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 900px;
            margin: 50px auto;
            padding: 20px;
        }
        .demo-section {
            background: #f9f9f9;
            padding: 20px;
            margin: 20px 0;
            border-radius: 8px;
        }
        button {
            padding: 10px 20px;
            margin: 5px;
            cursor: pointer;
            background: #2196F3;
            color: white;
            border: none;
            border-radius: 4px;
        }
        button:hover {
            background: #0b7dda;
        }
        button.danger {
            background: #f44336;
        }
        button.danger:hover {
            background: #d32f2f;
        }
        button:disabled {
            background: #cccccc;
            cursor: not-allowed;
        }
        .result {
            margin-top: 15px;
            padding: 15px;
            border-radius: 4px;
            font-family: monospace;
            white-space: pre-wrap;
        }
        .success {
            background: #d4edda;
            color: #155724;
            border-left: 4px solid #28a745;
        }
        .error {
            background: #f8d7da;
            color: #721c24;
            border-left: 4px solid #dc3545;
        }
        .info {
            background: #d1ecf1;
            color: #0c5460;
            border-left: 4px solid #17a2b8;
        }
    </style>
</head>
<body>
    <h1>Axios 请求取消机制演示</h1>

    <div class="demo-section">
        <h2>1. 基础取消示例</h2>
        <p>发起一个延迟 3 秒的请求,在请求完成前点击"取消请求"</p>
        <button id="send-btn" onclick="startRequest()">发送请求</button>
        <button id="cancel-btn" class="danger" onclick="cancelRequest()" disabled>取消请求</button>
        <div id="cancel-result" class="result"></div>
    </div>

    <div class="demo-section">
        <h2>2. 搜索框自动取消示例</h2>
        <p>输入搜索关键词时,自动取消上一个请求(防抖 + 取消)</p>
        <input type="text" id="search-input" placeholder="输入搜索关键词..." style="padding: 10px; width: 300px;">
        <div id="search-result" class="result"></div>
    </div>

    <script type="module">
        const defaults = {
            timeout: 0,
            responseType: 'json'
        };

        // 模拟 CancelToken(基于 Promise)
        class CancelToken {
            constructor(executor) {
                this.promise = new Promise((resolve) => {
                    this.resolve = resolve;
                });

                // 执行 executor,传入取消函数
                executor(() => {
                    this.resolve();
                });
            }
        }

        function dispatchRequest(config) {
            return new Promise((resolve, reject) => {
                const xhr = new XMLHttpRequest();
                xhr.responseType = config.responseType;
                xhr.timeout = config.timeout;
                
                xhr.open(config.method, config.url);

                if (config.headers) {
                    for (let key in config.headers) {
                        xhr.setRequestHeader(key, config.headers[key]);
                    }
                }

                let body;
                if (['POST', 'PUT', 'PATCH'].includes(config.method)) {
                    if (typeof config.data === 'string') {
                        body = config.data;
                    } else if (Object.prototype.toString.call(config.data) === '[object Object]') {
                        body = JSON.stringify(config.data);
                        if (!config.headers?.['Content-Type']) {
                            xhr.setRequestHeader('Content-type', 'application/json');
                        }
                    } else {
                        body = config.data;
                    }
                }

                // 设置取消请求的 Promise 状态改变
                if (config.cancelToken) {
                    config.cancelToken.promise.then(() => {
                        xhr.abort(); // 取消请求
                    });
                }

                xhr.send(body);

                xhr.onload = () => {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        resolve({
                            data: xhr.response,
                            status: xhr.status,
                            statusText: xhr.statusText,
                            config: config,
                            request: xhr
                        });
                    } else {
                        reject({
                            code: "ERR_BAD_REQUEST",
                            config,
                            message: `Request failed with status code ${xhr.status}`,
                            name: "AxiosError",
                            request: xhr
                        });
                    }
                };

                xhr.onerror = () => {
                    reject({
                        code: "ERR_NETWORK",
                        config,
                        message: "Network Error",
                        name: "AxiosError",
                        request: xhr
                    });
                };

                xhr.ontimeout = () => {
                    reject({
                        code: "ECONNABORTED",
                        config,
                        message: `timeout of ${config.timeout}ms exceeded`,
                        name: "AxiosError",
                        request: xhr
                    });
                };

                // 监听取消请求的事件
                xhr.onabort = () => {
                    reject({
                        code: "ERR_CANCELED",
                        message: "canceled",
                        name: "CanceledError"
                    });
                };
            });
        }

        class Axios {
            constructor(config) {
                this.defaults = config;
            }

            request(urlOrConfig, config = {}) {
                if (typeof urlOrConfig === 'string') {
                    config.url = urlOrConfig;
                } else {
                    config = urlOrConfig;
                }

                config = Object.assign({}, this.defaults, config);
                config.method = (config.method || 'get').toUpperCase();

                return dispatchRequest(config);
            }

            get(url, config = {}) {
                return this.request(url, { ...config, method: 'GET' });
            }

            post(url, data, config = {}) {
                return this.request(url, { ...config, data, method: 'POST' });
            }
        }

        function createInstance(defaultConfig) {
            const context = new Axios(defaultConfig);
            const instance = function(config) {
                return context.request(config);
            };

            instance.defaults = context.defaults;
            instance.CancelToken = CancelToken;
            instance.isCancel = (error) => error.code === "ERR_CANCELED";
            ['get', 'post', 'request'].forEach(method => {
                instance[method] = context[method].bind(context);
            });

            return instance;
        }

        const axios = createInstance(defaults);

        // === 示例 1:基础取消 ===
        let currentCancel = null;
        let isRequesting = false;

        window.startRequest = () => {
            if (isRequesting) {
                alert('请求正在进行中...');
                return;
            }

            const resultDiv = document.getElementById('cancel-result');
            const sendBtn = document.getElementById('send-btn');
            const cancelBtn = document.getElementById('cancel-btn');

            // 创建 CancelToken
            const source = axios.CancelToken((cancel) => {
                currentCancel = cancel;
            });

            isRequesting = true;
            sendBtn.disabled = true;
            cancelBtn.disabled = false;
            resultDiv.textContent = '发送请求...(3秒延迟)';
            resultDiv.className = 'result info';

            // 使用延迟 API 模拟长时间请求
            axios.get('https://delay.ms/3000/https://api.github.com/users/github', {
                cancelToken: source.promise
            })
            .then(response => {
                resultDiv.textContent = `成功:${response.data.name}\n仓库:${response.data.blog}`;
                resultDiv.className = 'result success';
            })
            .catch(error => {
                if (axios.isCancel(error)) {
                    resultDiv.textContent = '请求已被取消';
                    resultDiv.className = 'result info';
                } else {
                    resultDiv.textContent = `错误:${error.message}`;
                    resultDiv.className = 'result error';
                }
            })
            .finally(() => {
                isRequesting = false;
                sendBtn.disabled = false;
                cancelBtn.disabled = true;
                currentCancel = null;
            });
        };

        window.cancelRequest = () => {
            if (currentCancel) {
                currentCancel(); // 调用取消函数
                currentCancel = null;
            }
        };

        // === 示例 2:搜索框自动取消 ===
        let searchCancelToken = null;

        const searchInput = document.getElementById('search-input');
        let searchTimeout = null;

        searchInput.addEventListener('input', (e) => {
            const query = e.target.value.trim();
            const resultDiv = document.getElementById('search-result');

            // 清除之前的防抖定时器
            if (searchTimeout) {
                clearTimeout(searchTimeout);
            }

            // 取消上一个请求
            if (searchCancelToken) {
                searchCancelToken();
                searchCancelToken = null;
            }

            if (!query) {
                resultDiv.textContent = '请输入搜索关键词';
                resultDiv.className = 'result info';
                return;
            }

            resultDiv.textContent = '输入中...(防抖 500ms)';
            resultDiv.className = 'result info';

            // 防抖:500ms 后发送请求
            searchTimeout = setTimeout(() => {
                // 创建新的 CancelToken
                const source = axios.CancelToken((cancel) => {
                    searchCancelToken = cancel;
                });

                resultDiv.textContent = '搜索中...';
                resultDiv.className = 'result info';

                // 搜索 GitHub 用户
                axios.get(`https://api.github.com/search/users?q=${encodeURIComponent(query)}`, {
                    cancelToken: source.promise
                })
                .then(response => {
                    const count = response.data.total_count;
                    const users = response.data.items.slice(0, 5);
                    
                    let html = `找到 ${count} 个用户,前 5 个:\n`;
                    users.forEach(user => {
                        html += `\n- ${user.login} (${user.html_url})`;
                    });
                    
                    resultDiv.textContent = html;
                    resultDiv.className = 'result success';
                })
                .catch(error => {
                    if (axios.isCancel(error)) {
                        resultDiv.textContent = '已取消上一个搜索请求,正在发送新请求...';
                        resultDiv.className = 'result info';
                    } else {
                        resultDiv.textContent = `错误:${error.message}`;
                        resultDiv.className = 'result error';
                    }
                })
                .finally(() => {
                    searchCancelToken = null;
                });
            }, 500);
        });
    </script>
</body>
</html>

【代码注释】 取消机制的核心是 Promise 状态控制 ------CancelToken 内部维护一个 Promise,调用 cancel() 时 resolve 该 Promise,dispatchRequest 中监听到 resolve 后调用 xhr.abort()xhr.abort() 会触发 xhr.onabort 事件,此时 reject 错误对象,code: "ERR_CANCELED" 用于标识取消类型。市面应用 :React 组件卸载时取消请求(useEffect 清理函数)、搜索框输入自动取消上一次请求、路由跳转时取消未完成请求。

【实战要点】

  • 经典应用场景
    1. 组件卸载取消useEffect 返回清理函数 () => cancel(),避免已卸载组件更新状态
    2. 搜索框防抖:每次输入取消上一个请求,只保留最新请求
    3. 页面跳转取消router.beforeEach 中取消所有进行中的请求
  • 常见坑
    1. 忘记判断 axios.isCancel(error) 导致把取消错误当成普通错误处理
    2. CancelToken 旧语法(new axios.CancelToken())在新版本已废弃,应用 AbortController
    3. 取消后 xhr.status 为 0,与网络错误相同,需用 code 区分
  • 性能与最佳实践
    1. React 中用自定义 Hook useAxios 统一管理取消逻辑
    2. 多并发请求时用数组存储取消函数:const cancels = []; unmount() { cancels.forEach(c => c()); }
    3. 新项目推荐 AbortController(标准 API),兼容性更好

【本章小结】

方面 说明
取消原理 Promise 状态控制 + xhr.abort()
错误标识 code: "ERR_CANCELED"
判断方法 axios.isCancel(error)
新标准 AbortController(推荐)

记忆口诀"Cancel Promise 控状态,abort() 取消不彷徨;isCancel 判真伪,AbortController 新方向"

【面试考点】

Q5:axios 的请求取消原理是什么?

A:基于 Promise 状态控制 ------CancelToken 内部维护一个 Promise,调用 cancel() 时 resolve 该 Promise,dispatchRequest 监听到 resolve 后调用 xhr.abort() 取消请求。取消后 reject 的错误对象 code: "ERR_CANCELED",可用 axios.isCancel() 判断。追问"Promise 如何触发 XHR 取消"时答:通过 config.cancelToken.then(() => xhr.abort()) 实现,Promise resolve 时执行回调。


2.7 便捷方法实现

Axios 提供了 getpostputdelete 等便捷方法。
#mermaid-svg-whSMI4JyBh2spO3n{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-whSMI4JyBh2spO3n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-whSMI4JyBh2spO3n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-whSMI4JyBh2spO3n .error-icon{fill:#552222;}#mermaid-svg-whSMI4JyBh2spO3n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-whSMI4JyBh2spO3n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-whSMI4JyBh2spO3n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-whSMI4JyBh2spO3n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-whSMI4JyBh2spO3n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-whSMI4JyBh2spO3n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-whSMI4JyBh2spO3n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-whSMI4JyBh2spO3n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-whSMI4JyBh2spO3n .marker.cross{stroke:#333333;}#mermaid-svg-whSMI4JyBh2spO3n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-whSMI4JyBh2spO3n p{margin:0;}#mermaid-svg-whSMI4JyBh2spO3n .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-whSMI4JyBh2spO3n .cluster-label text{fill:#333;}#mermaid-svg-whSMI4JyBh2spO3n .cluster-label span{color:#333;}#mermaid-svg-whSMI4JyBh2spO3n .cluster-label span p{background-color:transparent;}#mermaid-svg-whSMI4JyBh2spO3n .label text,#mermaid-svg-whSMI4JyBh2spO3n span{fill:#333;color:#333;}#mermaid-svg-whSMI4JyBh2spO3n .node rect,#mermaid-svg-whSMI4JyBh2spO3n .node circle,#mermaid-svg-whSMI4JyBh2spO3n .node ellipse,#mermaid-svg-whSMI4JyBh2spO3n .node polygon,#mermaid-svg-whSMI4JyBh2spO3n .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-whSMI4JyBh2spO3n .rough-node .label text,#mermaid-svg-whSMI4JyBh2spO3n .node .label text,#mermaid-svg-whSMI4JyBh2spO3n .image-shape .label,#mermaid-svg-whSMI4JyBh2spO3n .icon-shape .label{text-anchor:middle;}#mermaid-svg-whSMI4JyBh2spO3n .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-whSMI4JyBh2spO3n .rough-node .label,#mermaid-svg-whSMI4JyBh2spO3n .node .label,#mermaid-svg-whSMI4JyBh2spO3n .image-shape .label,#mermaid-svg-whSMI4JyBh2spO3n .icon-shape .label{text-align:center;}#mermaid-svg-whSMI4JyBh2spO3n .node.clickable{cursor:pointer;}#mermaid-svg-whSMI4JyBh2spO3n .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-whSMI4JyBh2spO3n .arrowheadPath{fill:#333333;}#mermaid-svg-whSMI4JyBh2spO3n .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-whSMI4JyBh2spO3n .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-whSMI4JyBh2spO3n .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-whSMI4JyBh2spO3n .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-whSMI4JyBh2spO3n .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-whSMI4JyBh2spO3n .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-whSMI4JyBh2spO3n .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-whSMI4JyBh2spO3n .cluster text{fill:#333;}#mermaid-svg-whSMI4JyBh2spO3n .cluster span{color:#333;}#mermaid-svg-whSMI4JyBh2spO3n div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-whSMI4JyBh2spO3n .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-whSMI4JyBh2spO3n rect.text{fill:none;stroke-width:0;}#mermaid-svg-whSMI4JyBh2spO3n .icon-shape,#mermaid-svg-whSMI4JyBh2spO3n .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-whSMI4JyBh2spO3n .icon-shape p,#mermaid-svg-whSMI4JyBh2spO3n .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-whSMI4JyBh2spO3n .icon-shape .label rect,#mermaid-svg-whSMI4JyBh2spO3n .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-whSMI4JyBh2spO3n .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-whSMI4JyBh2spO3n .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-whSMI4JyBh2spO3n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} axios.get/url
调用 request
axios.post/url,data
调用 request
合并配置

method: GET
合并配置

method: POST, data
dispatchRequest

【代码注释】 此图展示便捷方法的实现原理------所有便捷方法最终都调用 request() 方法,只是自动填充了 HTTP 方法和参数。get(url, config) 转换为 request(url, {...config, method: 'GET'})post(url, data, config) 转换为 request(url, {...config, data, method: 'POST'})。这种设计避免重复代码,所有配置合并、拦截器逻辑都在 request 中统一处理。

实战示例:便捷方法完整实现
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Axios 便捷方法实现</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 1000px;
            margin: 50px auto;
            padding: 20px;
        }
        .method-section {
            background: #f9f9f9;
            padding: 20px;
            margin: 20px 0;
            border-radius: 8px;
        }
        button {
            padding: 10px 20px;
            margin: 5px;
            cursor: pointer;
            background: #2196F3;
            color: white;
            border: none;
            border-radius: 4px;
        }
        button:hover {
            background: #0b7dda;
        }
        .get { background: #4CAF50; }
        .post { background: #2196F3; }
        .put { background: #FF9800; }
        .delete { background: #f44336; }
        
        .result {
            margin-top: 15px;
            padding: 15px;
            border-radius: 4px;
            background: white;
            font-family: monospace;
            white-space: pre-wrap;
            max-height: 400px;
            overflow-y: auto;
        }
    </style>
</head>
<body>
    <h1>Axios 便捷方法演示</h1>

    <div class="method-section">
        <h2>GET 请求</h2>
        <button class="get" onclick="testGet()">GET 请求示例</button>
        <div id="get-result" class="result"></div>
    </div>

    <div class="method-section">
        <h2>POST 请求</h2>
        <button class="post" onclick="testPost()">POST 请求示例</button>
        <div id="post-result" class="result"></div>
    </div>

    <div class="method-section">
        <h2>PUT 请求</h2>
        <button class="put" onclick="testPut()">PUT 请求示例</button>
        <div id="put-result" class="result"></div>
    </div>

    <div class="method-section">
        <h2>DELETE 请求</h2>
        <button class="delete" onclick="testDelete()">DELETE 请求示例</button>
        <div id="delete-result" class="result"></div>
    </div>

    <script type="module">
        const defaults = {
            timeout: 10000,
            responseType: 'json'
        };

        function dispatchRequest(config) {
            return new Promise((resolve, reject) => {
                const xhr = new XMLHttpRequest();
                xhr.responseType = config.responseType;
                xhr.timeout = config.timeout;
                
                xhr.open(config.method, config.url);

                if (config.headers) {
                    for (let key in config.headers) {
                        xhr.setRequestHeader(key, config.headers[key]);
                    }
                }

                let body;
                if (['POST', 'PUT', 'PATCH'].includes(config.method)) {
                    if (typeof config.data === 'string') {
                        body = config.data;
                    } else if (Object.prototype.toString.call(config.data) === '[object Object]') {
                        body = JSON.stringify(config.data);
                        if (!config.headers?.['Content-Type']) {
                            xhr.setRequestHeader('Content-type', 'application/json');
                        }
                    } else {
                        body = config.data;
                    }
                }

                xhr.send(body);

                xhr.onload = () => {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        resolve({
                            data: xhr.response,
                            status: xhr.status,
                            statusText: xhr.statusText,
                            config: config,
                            request: xhr
                        });
                    } else {
                        reject({
                            code: "ERR_BAD_REQUEST",
                            config,
                            message: `Request failed with status code ${xhr.status}`,
                            name: "AxiosError",
                            request: xhr
                        });
                    }
                };

                xhr.onerror = () => {
                    reject({
                        code: "ERR_NETWORK",
                        config,
                        message: "Network Error",
                        name: "AxiosError",
                        request: xhr
                    });
                };

                xhr.ontimeout = () => {
                    reject({
                        code: "ECONNABORTED",
                        config,
                        message: `timeout of ${config.timeout}ms exceeded`,
                        name: "AxiosError",
                        request: xhr
                    });
                };
            });
        }

        class Axios {
            constructor(config) {
                this.defaults = config;
            }

            request(urlOrConfig, config = {}) {
                if (typeof urlOrConfig === 'string') {
                    config.url = urlOrConfig;
                } else {
                    config = urlOrConfig;
                }

                config = Object.assign({}, this.defaults, config);
                config.method = (config.method || 'get').toUpperCase();

                return dispatchRequest(config);
            }

            get(url, config = {}) {
                return this.request(url, { ...config, method: 'GET' });
            }

            post(url, data, config = {}) {
                return this.request(url, { ...config, data, method: 'POST' });
            }

            put(url, data, config = {}) {
                return this.request(url, { ...config, data, method: 'PUT' });
            }

            delete(url, config = {}) {
                return this.request(url, { ...config, method: 'DELETE' });
            }

            patch(url, data, config = {}) {
                return this.request(url, { ...config, data, method: 'PATCH' });
            }
        }

        function createInstance(defaultConfig) {
            const context = new Axios(defaultConfig);
            const instance = function(config) {
                return context.request(config);
            };

            instance.defaults = context.defaults;
            
            // 将实例的所有属性复制到 instance 上
            for (let key in context) {
                instance[key] = context[key];
            }

            // 将原型的所有方法绑定到 context 后添加到 instance 上
            Object.getOwnPropertyNames(Axios.prototype).forEach(key => {
                instance[key] = Axios.prototype[key].bind(context);
            });

            return instance;
        }

        const axios = createInstance(defaults);

        // === 测试函数 ===
        window.testGet = () => {
            const resultDiv = document.getElementById('get-result');
            resultDiv.textContent = '发送 GET 请求...';

            // 方式1:使用 get 方法
            axios.get('https://api.github.com/users/github')
                .then(response => {
                    resultDiv.textContent = `GET 请求成功!\n\n` +
                        `用户名: ${response.data.name}\n` +
                        `登录名: ${response.data.login}\n` +
                        `仓库数: ${response.data.public_repos}\n` +
                        `位置: ${response.data.location}\n` +
                        `博客: ${response.data.blog}`;
                })
                .catch(error => {
                    resultDiv.textContent = `错误: ${error.message}`;
                });
        };

        window.testPost = () => {
            const resultDiv = document.getElementById('post-result');
            resultDiv.textContent = '发送 POST 请求...';

            // 使用 JSONPlaceholder 测试 POST
            axios.post('https://jsonplaceholder.typicode.com/posts', {
                title: 'foo',
                body: 'bar',
                userId: 1
            })
            .then(response => {
                resultDiv.textContent = `POST 请求成功!\n\n` +
                    `响应状态: ${response.status}\n` +
                    `创建的资源 ID: ${response.data.id}\n` +
                    `请求数据:\n${JSON.stringify({title: 'foo', body: 'bar', userId: 1}, null, 2)}\n\n` +
                    `响应数据:\n${JSON.stringify(response.data, null, 2)}`;
            })
            .catch(error => {
                resultDiv.textContent = `错误: ${error.message}`;
            });
        };

        window.testPut = () => {
            const resultDiv = document.getElementById('put-result');
            resultDiv.textContent = '发送 PUT 请求...';

            // PUT 更新资源
            axios.put('https://jsonplaceholder.typicode.com/posts/1', {
                id: 1,
                title: 'updated title',
                body: 'updated body',
                userId: 1
            })
            .then(response => {
                resultDiv.textContent = `PUT 请求成功!\n\n` +
                    `响应状态: ${response.status}\n` +
                    `更新后的数据:\n${JSON.stringify(response.data, null, 2)}`;
            })
            .catch(error => {
                resultDiv.textContent = `错误: ${error.message}`;
            });
        };

        window.testDelete = () => {
            const resultDiv = document.getElementById('delete-result');
            resultDiv.textContent = '发送 DELETE 请求...';

            // DELETE 删除资源
            axios.delete('https://jsonplaceholder.typicode.com/posts/1')
                .then(response => {
                    resultDiv.textContent = `DELETE 请求成功!\n\n` +
                        `响应状态: ${response.status}\n` +
                        `响应数据:\n${JSON.stringify(response.data, null, 2)}`;
                })
                .catch(error => {
                    resultDiv.textContent = `错误: ${error.message}`;
                });
        };
    </script>
</body>
</html>

【代码注释】 便捷方法的核心是参数转发与配置合并 ------get(url, config) 转成 request(url, {...config, method: 'GET'})post(url, data, config) 转成 request(url, {...config, data, method: 'POST'})。实现时要注意 data 参数只对 POST/PUT/PATCH 有效,GET/DELETE 请求的参数用 config.params市面应用 :RESTful API 调用几乎全用便捷方法,如 axios.get('/users')axios.post('/users', userData),比 axios({method: 'POST', url: '/users', data: userData}) 简洁。

【本章小结】

方法 参数 请求体 用途
get(url, config) params 在 config 中 获取资源
post(url, data, config) data 是请求体 创建资源
put(url, data, config) data 是请求体 全量更新
patch(url, data, config) data 是请求体 部分更新
delete(url, config) params 在 config 中 删除资源

记忆口诀"get/delete 读资源,post/put 写数据;patch 改部分,put 改全部"


三、拦截器机制深度剖析

3.1 拦截器核心概念

拦截器是 Axios 最强大的功能之一,采用责任链模式实现。
#mermaid-svg-Tqhvj2yUbZTNmM3v{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Tqhvj2yUbZTNmM3v .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Tqhvj2yUbZTNmM3v .error-icon{fill:#552222;}#mermaid-svg-Tqhvj2yUbZTNmM3v .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Tqhvj2yUbZTNmM3v .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Tqhvj2yUbZTNmM3v .marker.cross{stroke:#333333;}#mermaid-svg-Tqhvj2yUbZTNmM3v svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Tqhvj2yUbZTNmM3v p{margin:0;}#mermaid-svg-Tqhvj2yUbZTNmM3v .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Tqhvj2yUbZTNmM3v .cluster-label text{fill:#333;}#mermaid-svg-Tqhvj2yUbZTNmM3v .cluster-label span{color:#333;}#mermaid-svg-Tqhvj2yUbZTNmM3v .cluster-label span p{background-color:transparent;}#mermaid-svg-Tqhvj2yUbZTNmM3v .label text,#mermaid-svg-Tqhvj2yUbZTNmM3v span{fill:#333;color:#333;}#mermaid-svg-Tqhvj2yUbZTNmM3v .node rect,#mermaid-svg-Tqhvj2yUbZTNmM3v .node circle,#mermaid-svg-Tqhvj2yUbZTNmM3v .node ellipse,#mermaid-svg-Tqhvj2yUbZTNmM3v .node polygon,#mermaid-svg-Tqhvj2yUbZTNmM3v .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Tqhvj2yUbZTNmM3v .rough-node .label text,#mermaid-svg-Tqhvj2yUbZTNmM3v .node .label text,#mermaid-svg-Tqhvj2yUbZTNmM3v .image-shape .label,#mermaid-svg-Tqhvj2yUbZTNmM3v .icon-shape .label{text-anchor:middle;}#mermaid-svg-Tqhvj2yUbZTNmM3v .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Tqhvj2yUbZTNmM3v .rough-node .label,#mermaid-svg-Tqhvj2yUbZTNmM3v .node .label,#mermaid-svg-Tqhvj2yUbZTNmM3v .image-shape .label,#mermaid-svg-Tqhvj2yUbZTNmM3v .icon-shape .label{text-align:center;}#mermaid-svg-Tqhvj2yUbZTNmM3v .node.clickable{cursor:pointer;}#mermaid-svg-Tqhvj2yUbZTNmM3v .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Tqhvj2yUbZTNmM3v .arrowheadPath{fill:#333333;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Tqhvj2yUbZTNmM3v .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Tqhvj2yUbZTNmM3v .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tqhvj2yUbZTNmM3v .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Tqhvj2yUbZTNmM3v .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Tqhvj2yUbZTNmM3v .cluster text{fill:#333;}#mermaid-svg-Tqhvj2yUbZTNmM3v .cluster span{color:#333;}#mermaid-svg-Tqhvj2yUbZTNmM3v div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Tqhvj2yUbZTNmM3v .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Tqhvj2yUbZTNmM3v rect.text{fill:none;stroke-width:0;}#mermaid-svg-Tqhvj2yUbZTNmM3v .icon-shape,#mermaid-svg-Tqhvj2yUbZTNmM3v .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tqhvj2yUbZTNmM3v .icon-shape p,#mermaid-svg-Tqhvj2yUbZTNmM3v .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Tqhvj2yUbZTNmM3v .icon-shape .label rect,#mermaid-svg-Tqhvj2yUbZTNmM3v .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tqhvj2yUbZTNmM3v .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Tqhvj2yUbZTNmM3v .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Tqhvj2yUbZTNmM3v :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求配置
请求拦截器 2

后添加先执行
请求拦截器 1

先添加后执行
dispatchRequest

发送请求
响应拦截器 1

先添加先执行
响应拦截器 2

后添加后执行
响应数据

【代码注释】 此图展示拦截器的执行顺序------请求拦截器采用后进先出(LIFO) ,后添加的先执行;响应拦截器采用先进先出(FIFO) ,先添加的先执行。实现上通过数组操作完成:请求拦截器用 unshift 插到数组前面,响应拦截器用 push 插到后面。这种设计让后注册的请求拦截器能优先处理配置(如最后添加的身份认证),而先注册的响应拦截器能优先处理响应(如第一个添加的数据转换)。

名词解释

  • 请求拦截器:在请求发送前执行,可修改配置、添加 token
  • 响应拦截器:在响应返回后执行,可统一处理错误、提取数据
  • 责任链模式:将处理器串成链,依次处理请求/响应

3.2 拦截器实现原理

拦截器通过 Promise 链式调用实现异步串联。
ResInterceptor2 ResInterceptor1 Dispatch ReqInterceptor2 ReqInterceptor1 Client ResInterceptor2 ResInterceptor1 Dispatch ReqInterceptor2 ReqInterceptor1 Client #mermaid-svg-9gDOukLKWjE1yQX8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9gDOukLKWjE1yQX8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9gDOukLKWjE1yQX8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9gDOukLKWjE1yQX8 .error-icon{fill:#552222;}#mermaid-svg-9gDOukLKWjE1yQX8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9gDOukLKWjE1yQX8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9gDOukLKWjE1yQX8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9gDOukLKWjE1yQX8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9gDOukLKWjE1yQX8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9gDOukLKWjE1yQX8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9gDOukLKWjE1yQX8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9gDOukLKWjE1yQX8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9gDOukLKWjE1yQX8 .marker.cross{stroke:#333333;}#mermaid-svg-9gDOukLKWjE1yQX8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9gDOukLKWjE1yQX8 p{margin:0;}#mermaid-svg-9gDOukLKWjE1yQX8 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9gDOukLKWjE1yQX8 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-9gDOukLKWjE1yQX8 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9gDOukLKWjE1yQX8 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-9gDOukLKWjE1yQX8 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-9gDOukLKWjE1yQX8 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-9gDOukLKWjE1yQX8 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-9gDOukLKWjE1yQX8 .sequenceNumber{fill:white;}#mermaid-svg-9gDOukLKWjE1yQX8 #sequencenumber{fill:#333;}#mermaid-svg-9gDOukLKWjE1yQX8 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-9gDOukLKWjE1yQX8 .messageText{fill:#333;stroke:none;}#mermaid-svg-9gDOukLKWjE1yQX8 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9gDOukLKWjE1yQX8 .labelText,#mermaid-svg-9gDOukLKWjE1yQX8 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-9gDOukLKWjE1yQX8 .loopText,#mermaid-svg-9gDOukLKWjE1yQX8 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-9gDOukLKWjE1yQX8 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9gDOukLKWjE1yQX8 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-9gDOukLKWjE1yQX8 .noteText,#mermaid-svg-9gDOukLKWjE1yQX8 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-9gDOukLKWjE1yQX8 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9gDOukLKWjE1yQX8 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9gDOukLKWjE1yQX8 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9gDOukLKWjE1yQX8 .actorPopupMenu{position:absolute;}#mermaid-svg-9gDOukLKWjE1yQX8 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-9gDOukLKWjE1yQX8 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9gDOukLKWjE1yQX8 .actor-man circle,#mermaid-svg-9gDOukLKWjE1yQX8 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-9gDOukLKWjE1yQX8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} config config config response response response

【代码注释】 此序列图展示拦截器的数据流向------请求配置经过所有请求拦截器处理后到达 dispatchRequest,响应数据再经过所有响应拦截器处理后返回客户端。核心实现是 Promise 链chain 数组存储 [fulfilledHandler, rejectedHandler] 对,通过 while (chain.length) { promise = promise.then(...chain.shift()); } 串联成链。每个拦截器的返回值成为下一个拦截器的输入,形成完整的处理管道。

入门示例:拦截器基础实现
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Axios 拦截器实现原理</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 1000px;
            margin: 50px auto;
            padding: 20px;
        }
        .demo-box {
            background: #f9f9f9;
            padding: 20px;
            margin: 20px 0;
            border-radius: 8px;
        }
        button {
            padding: 10px 20px;
            margin: 5px;
            cursor: pointer;
            background: #2196F3;
            color: white;
            border: none;
            border-radius: 4px;
        }
        .log-box {
            background: #263238;
            color: #aed581;
            padding: 15px;
            border-radius: 4px;
            font-family: 'Courier New', monospace;
            white-space: pre-wrap;
            max-height: 500px;
            overflow-y: auto;
        }
        .log-entry {
            margin: 5px 0;
            padding: 5px;
            border-left: 3px solid transparent;
        }
        .request { border-left-color: #4CAF50; }
        .response { border-left-color: #2196F3; }
        .error { border-left-color: #f44336; }
    </style>
</head>
<body>
    <h1>Axios 拦截器实现原理演示</h1>

    <div class="demo-box">
        <h2>1. 拦截器执行顺序测试</h2>
        <button onclick="testOrder()">测试拦截器执行顺序</button>
        <div id="log-box" class="log-box"></div>
    </div>

    <div class="demo-box">
        <h2>2. 请求拦截器:添加 Token</h2>
        <button onclick="testToken()">测试自动添加 Token</button>
        <div id="token-result" class="log-box"></div>
    </div>

    <div class="demo-box">
        <h2>3. 响应拦截器:统一错误处理</h3>
        <button onclick="testErrorHandler()">测试统一错误处理</button>
        <div id="error-result" class="log-box"></div>
    </div>

    <script type="module">
        // 拦截器管理器
        class InterceptorManager {
            constructor() {
                this.handlers = [];
            }

            use(onResolved, onRejected) {
                // 添加拦截器到数组
                this.handlers.push({
                    onResolved,
                    onRejected
                });
                // 返回索引,用于 eject
                return this.handlers.length - 1;
            }

            eject(id) {
                // 移除指定拦截器
                if (this.handlers[id]) {
                    this.handlers[id] = null;
                }
            }

            forEach(fn) {
                // 遍历所有拦截器
                this.handlers.forEach(handler => {
                    if (handler !== null) {
                        fn(handler);
                    }
                });
            }
        }

        const defaults = {
            timeout: 10000,
            responseType: 'json'
        };

        function dispatchRequest(config) {
            return new Promise((resolve, reject) => {
                const xhr = new XMLHttpRequest();
                xhr.responseType = config.responseType;
                xhr.timeout = config.timeout;
                
                xhr.open(config.method, config.url);

                if (config.headers) {
                    for (let key in config.headers) {
                        xhr.setRequestHeader(key, config.headers[key]);
                    }
                }

                let body;
                if (['POST', 'PUT', 'PATCH'].includes(config.method)) {
                    if (typeof config.data === 'string') {
                        body = config.data;
                    } else if (Object.prototype.toString.call(config.data) === '[object Object]') {
                        body = JSON.stringify(config.data);
                        if (!config.headers?.['Content-Type']) {
                            xhr.setRequestHeader('Content-type', 'application/json');
                        }
                    } else {
                        body = config.data;
                    }
                }

                xhr.send(body);

                xhr.onload = () => {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        resolve({
                            data: xhr.response,
                            status: xhr.status,
                            statusText: xhr.statusText,
                            config: config,
                            request: xhr
                        });
                    } else {
                        reject({
                            code: "ERR_BAD_REQUEST",
                            config,
                            message: `Request failed with status code ${xhr.status}`,
                            name: "AxiosError",
                            request: xhr,
                            response: {
                                data: xhr.response,
                                status: xhr.status,
                                statusText: xhr.statusText
                            }
                        });
                    }
                };

                xhr.onerror = () => {
                    reject({
                        code: "ERR_NETWORK",
                        config,
                        message: "Network Error",
                        name: "AxiosError",
                        request: xhr
                    });
                };

                xhr.ontimeout = () => {
                    reject({
                        code: "ECONNABORTED",
                        config,
                        message: `timeout of ${config.timeout}ms exceeded`,
                        name: "AxiosError",
                        request: xhr
                    });
                };
            });
        }

        class Axios {
            constructor(config) {
                this.defaults = config;
                this.interceptors = {
                    request: new InterceptorManager(),
                    response: new InterceptorManager()
                };
            }

            request(urlOrConfig, config = {}) {
                if (typeof urlOrConfig === 'string') {
                    config.url = urlOrConfig;
                } else {
                    config = urlOrConfig;
                }

                config = Object.assign({}, this.defaults, config);
                config.method = (config.method || 'get').toUpperCase();

                // === 核心:构建拦截器链 ===
                
                // 1. 创建执行链,初始包含 dispatchRequest
                const chain = [[dispatchRequest.bind(this, config), undefined]];

                // 2. 将所有请求拦截器添加到链的前面(后进先出)
                this.interceptors.request.handlers.forEach(handler => {
                    if (handler !== null) {
                        chain.unshift([handler.onResolved, handler.onRejected]);
                    }
                });

                // 3. 将所有响应拦截器添加到链的后面(先进先出)
                this.interceptors.response.handlers.forEach(handler => {
                    if (handler !== null) {
                        chain.push([handler.onResolved, handler.onRejected]);
                    }
                });

                // 4. 使用 Promise 链式调用执行
                let promise = Promise.resolve(config);

                while (chain.length) {
                    // shift() 取出第一个元素并从数组中删除
                    promise = promise.then(...chain.shift());
                }

                return promise;
            }

            get(url, config = {}) {
                return this.request(url, { ...config, method: 'GET' });
            }

            post(url, data, config = {}) {
                return this.request(url, { ...config, data, method: 'POST' });
            }
        }

        function createInstance(defaultConfig) {
            const context = new Axios(defaultConfig);
            const instance = function(config) {
                return context.request(config);
            };

            instance.defaults = context.defaults;
            instance.interceptors = context.interceptors;

            for (let key in context) {
                if (key !== 'interceptors') {
                    instance[key] = context[key];
                }
            }

            Object.getOwnPropertyNames(Axios.prototype).forEach(key => {
                if (key !== 'constructor') {
                    instance[key] = Axios.prototype[key].bind(context);
                }
            });

            return instance;
        }

        const axios = createInstance(defaults);

        // === 测试函数 ===
        function addLog(elementId, message, type = 'request') {
            const logBox = document.getElementById(elementId);
            const entry = document.createElement('div');
            entry.className = `log-entry ${type}`;
            
            const timestamp = new Date().toLocaleTimeString();
            entry.textContent = `[${timestamp}] ${message}`;
            
            logBox.appendChild(entry);
            logBox.scrollTop = logBox.scrollHeight;
        }

        window.testOrder = () => {
            const logBox = document.getElementById('log-box');
            logBox.innerHTML = '';
            
            // 清除之前的拦截器
            axios.interceptors.request.handlers = [];
            axios.interceptors.response.handlers = [];

            // 添加请求拦截器
            axios.interceptors.request.use(
                config => {
                    addLog('log-box', '请求拦截器 1:添加配置', 'request');
                    config.metadata = { startTime: Date.now() };
                    return config;
                },
                error => {
                    addLog('log-box', '请求拦截器 1:错误', 'error');
                    return Promise.reject(error);
                }
            );

            axios.interceptors.request.use(
                config => {
                    addLog('log-box', '请求拦截器 2:添加 headers', 'request');
                    config.headers['X-Custom-Header'] = 'test-value';
                    return config;
                },
                error => {
                    addLog('log-box', '请求拦截器 2:错误', 'error');
                    return Promise.reject(error);
                }
            );

            // 添加响应拦截器
            axios.interceptors.response.use(
                response => {
                    addLog('log-box', '响应拦截器 1:处理响应数据', 'response');
                    return response;
                },
                error => {
                    addLog('log-box', '响应拦截器 1:处理错误', 'error');
                    return Promise.reject(error);
                }
            );

            axios.interceptors.response.use(
                response => {
                    const duration = Date.now() - response.config.metadata.startTime;
                    addLog('log-box', `响应拦截器 2:计算耗时 ${duration}ms`, 'response');
                    return response;
                },
                error => {
                    addLog('log-box', '响应拦截器 2:错误', 'error');
                    return Promise.reject(error);
                }
            );

            // 发送请求
            axios.get('https://api.github.com/users/github')
                .then(response => {
                    addLog('log-box', `最终结果:${response.data.name}`, 'response');
                })
                .catch(error => {
                    addLog('log-box', `最终错误:${error.message}`, 'error');
                });
        };

        window.testToken = () => {
            const resultDiv = document.getElementById('token-result');
            resultDiv.innerHTML = '';

            // 清除之前的拦截器
            axios.interceptors.request.handlers = [];
            axios.interceptors.response.handlers = [];

            // 模拟 token 存储
            const token = 'mock-jwt-token-12345';

            // 添加 token 拦截器
            axios.interceptors.request.use(
                config => {
                    addLog('token-result', `请求拦截器:为 ${config.url} 添加 token`, 'request');
                    
                    // 为所有请求添加 Authorization 头
                    config.headers['Authorization'] = `Bearer ${token}`;
                    
                    addLog('token-result', `Authorization: Bearer ${token}`, 'request');
                    return config;
                },
                error => {
                    return Promise.reject(error);
                }
            );

            // 添加响应拦截器处理 token 过期
            axios.interceptors.response.use(
                response => {
                    return response;
                },
                error => {
                    if (error.response?.status === 401) {
                        addLog('token-result', '响应拦截器:Token 过期,需重新登录', 'error');
                        // 这里可以跳转到登录页
                    }
                    return Promise.reject(error);
                }
            );

            // 发送请求
            axios.get('https://api.github.com/users/github')
                .then(response => {
                    addLog('token-result', `请求成功:${response.data.name}`, 'response');
                    addLog('token-result', `请求头包含:${Object.keys(response.config.headers).join(', ')}`, 'response');
                })
                .catch(error => {
                    addLog('token-result', `请求失败:${error.message}`, 'error');
                });
        };

        window.testErrorHandler = () => {
            const resultDiv = document.getElementById('error-result');
            resultDiv.innerHTML = '';

            // 清除之前的拦截器
            axios.interceptors.request.handlers = [];
            axios.interceptors.response.handlers = [];

            // 统一错误处理拦截器
            axios.interceptors.response.use(
                response => {
                    // 成功响应直接返回
                    return response;
                },
                error => {
                    addLog('error-result', `统一错误处理:${error.code}`, 'error');

                    // 根据错误类型返回用户友好的错误信息
                    if (error.code === 'ERR_NETWORK') {
                        addLog('error-result', '错误:网络连接失败,请检查网络', 'error');
                        return Promise.reject({
                            ...error,
                            userMessage: '网络连接失败,请检查您的网络设置'
                        });
                    } else if (error.code === 'ERR_BAD_REQUEST') {
                        const status = error.response?.status;
                        if (status === 404) {
                            addLog('error-result', '错误:请求的资源不存在', 'error');
                            return Promise.reject({
                                ...error,
                                userMessage: '请求的资源不存在'
                            });
                        } else if (status >= 500) {
                            addLog('error-result', '错误:服务器错误', 'error');
                            return Promise.reject({
                                ...error,
                                userMessage: '服务器出现错误,请稍后重试'
                            });
                        }
                    } else if (error.code === 'ECONNABORTED') {
                        addLog('error-result', '错误:请求超时', 'error');
                        return Promise.reject({
                            ...error,
                            userMessage: '请求超时,请稍后重试'
                        });
                    }

                    return Promise.reject(error);
                }
            );

            // 测试 404 错误
            axios.get('https://api.github.com/users/nonexistent-user-12345')
                .then(response => {
                    addLog('error-result', `成功:${response.data.name}`, 'response');
                })
                .catch(error => {
                    addLog('error-result', `用户友好的错误消息:${error.userMessage || error.message}`, 'error');
                });
        };
    </script>
</body>
</html>

【代码注释】 拦截器链的核心是 Promise 链式调用 ------chain 数组存储 [fulfilledHandler, rejectedHandler] 对,请求拦截器用 unshift 插到数组前面(后进先出),响应拦截器用 push 插到后面(先进先出)。promise.then(...chain.shift()) 每次取出第一个处理器并串联成 Promise 链。市面应用:Vue Router 的导航守卫、Express 的中间件、Redux 的中间件都用类似机制,只是实现细节不同。

【实战要点】

  • 经典应用场景
    1. 身份认证 :请求拦截器自动添加 Authorization: Bearer token
    2. 错误处理:响应拦截器统一处理 401(跳转登录)、500(提示错误)
    3. 请求日志:记录请求耗时、参数,用于调试
    4. 数据转换 :响应拦截器自动提取 response.data
  • 常见坑
    1. 请求拦截器必须返回 config,否则请求丢失配置
    2. 响应拦截器必须返回 responsePromise.reject(error),否则后续拦截器接收不到数据
    3. 异步拦截器必须返回 Promise,否则链式调用中断
  • 性能与最佳实践
    1. 拦截器中避免耗时操作(如复杂计算、同步请求)
    2. 错误拦截器用 Promise.reject(error) 保持错误链
    3. 多拦截器时注意顺序------身份认证 → 请求日志 → 参数处理

【本章小结】

方面 请求拦截器 响应拦截器
执行时机 请求发送前 响应返回后
执行顺序 后进先出(LIFO) 先进先出(FIFO)
接收参数 config response
返回值 config response
常见用途 添加 token、日志、参数处理 错误处理、数据转换、loading

记忆口诀"请求拦截后先出,响应拦截先先出;request 加认证,response 处错误"

【面试考点】

Q6:axios 拦截器的执行顺序是什么?为什么?

A:请求拦截器是后进先出(LIFO) ,因为用 unshift 插到数组前面;响应拦截器是先进先出(FIFO) ,因为用 push 插到后面。这样设计让后注册的请求拦截器先执行(类似栈),先注册的响应拦截器先执行(类似队列)。追问"如何让请求拦截器按注册顺序执行"时答:改用 push 插入,但需调整 dispatchRequest 的插入位置。

Q7:拦截器中如何处理异步操作?

A:拦截器必须返回 Promise ------异步操作用 async/await 或返回 Promise.then()。如 async config => { const token = await getToken(); config.headers.Authorization = token; return config; }。如果拦截器不返回 Promise,后续拦截器会收到 undefined 而非配置对象。

3.3 拦截器实战应用

生产里拦截器常组合成「横切关注点」管道,典型分层如下:
#mermaid-svg-bEliazkaz91nYMOq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bEliazkaz91nYMOq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bEliazkaz91nYMOq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bEliazkaz91nYMOq .error-icon{fill:#552222;}#mermaid-svg-bEliazkaz91nYMOq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bEliazkaz91nYMOq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bEliazkaz91nYMOq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bEliazkaz91nYMOq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bEliazkaz91nYMOq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bEliazkaz91nYMOq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bEliazkaz91nYMOq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bEliazkaz91nYMOq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bEliazkaz91nYMOq .marker.cross{stroke:#333333;}#mermaid-svg-bEliazkaz91nYMOq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bEliazkaz91nYMOq p{margin:0;}#mermaid-svg-bEliazkaz91nYMOq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bEliazkaz91nYMOq .cluster-label text{fill:#333;}#mermaid-svg-bEliazkaz91nYMOq .cluster-label span{color:#333;}#mermaid-svg-bEliazkaz91nYMOq .cluster-label span p{background-color:transparent;}#mermaid-svg-bEliazkaz91nYMOq .label text,#mermaid-svg-bEliazkaz91nYMOq span{fill:#333;color:#333;}#mermaid-svg-bEliazkaz91nYMOq .node rect,#mermaid-svg-bEliazkaz91nYMOq .node circle,#mermaid-svg-bEliazkaz91nYMOq .node ellipse,#mermaid-svg-bEliazkaz91nYMOq .node polygon,#mermaid-svg-bEliazkaz91nYMOq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bEliazkaz91nYMOq .rough-node .label text,#mermaid-svg-bEliazkaz91nYMOq .node .label text,#mermaid-svg-bEliazkaz91nYMOq .image-shape .label,#mermaid-svg-bEliazkaz91nYMOq .icon-shape .label{text-anchor:middle;}#mermaid-svg-bEliazkaz91nYMOq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bEliazkaz91nYMOq .rough-node .label,#mermaid-svg-bEliazkaz91nYMOq .node .label,#mermaid-svg-bEliazkaz91nYMOq .image-shape .label,#mermaid-svg-bEliazkaz91nYMOq .icon-shape .label{text-align:center;}#mermaid-svg-bEliazkaz91nYMOq .node.clickable{cursor:pointer;}#mermaid-svg-bEliazkaz91nYMOq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bEliazkaz91nYMOq .arrowheadPath{fill:#333333;}#mermaid-svg-bEliazkaz91nYMOq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bEliazkaz91nYMOq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bEliazkaz91nYMOq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bEliazkaz91nYMOq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bEliazkaz91nYMOq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bEliazkaz91nYMOq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bEliazkaz91nYMOq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bEliazkaz91nYMOq .cluster text{fill:#333;}#mermaid-svg-bEliazkaz91nYMOq .cluster span{color:#333;}#mermaid-svg-bEliazkaz91nYMOq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bEliazkaz91nYMOq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bEliazkaz91nYMOq rect.text{fill:none;stroke-width:0;}#mermaid-svg-bEliazkaz91nYMOq .icon-shape,#mermaid-svg-bEliazkaz91nYMOq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bEliazkaz91nYMOq .icon-shape p,#mermaid-svg-bEliazkaz91nYMOq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bEliazkaz91nYMOq .icon-shape .label rect,#mermaid-svg-bEliazkaz91nYMOq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bEliazkaz91nYMOq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bEliazkaz91nYMOq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bEliazkaz91nYMOq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 响应拦截器 由内到外
解包 data
401 跳登录
Loading 计数 -1
请求拦截器 由外到内
traceId / 时间戳
Token / Cookie
Loading 计数 +1
真实请求

【代码注释】

流程图体现拦截器的注册顺序与执行顺序相反/相同 :请求侧后注册的(如 Token)先执行,保证离网络最近的一层最后改 config;响应侧先注册的先执行,先解包再统一 401。Loading 放在请求链末尾、响应链末尾配对,才能包住整次往返耗时。

实战示例:Loading + Token + 业务码解包(伪代码骨架)

javascript 复制代码
let pendingCount = 0;

function showLoading() {
  if (pendingCount++ === 0) document.getElementById('loading').style.display = 'block';
}
function hideLoading() {
  if (--pendingCount <= 0) {
    pendingCount = 0;
    document.getElementById('loading').style.display = 'none';
  }
}

axios.interceptors.request.use((config) => {
  showLoading();
  const token = localStorage.getItem('token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  config.headers['X-Request-Id'] = crypto.randomUUID?.() || String(Date.now());
  return config;
});

axios.interceptors.response.use(
  (response) => {
    hideLoading();
    // 后端约定 { code: 0, data, msg }
    const body = response.data;
    if (body && typeof body.code === 'number' && body.code !== 0) {
      return Promise.reject({ ...response, userMessage: body.msg || '业务失败' });
    }
    return body?.data !== undefined ? { ...response, data: body.data } : response;
  },
  (error) => {
    hideLoading();
    if (error.response?.status === 401) {
      location.href = '/login';
    }
    return Promise.reject(error);
  }
);

【代码注释】

上述骨架演示生产里三层拦截器的典型分工:pendingCount 控制全局 Loading,避免并行请求导致遮罩闪烁;请求阶段注入 AuthorizationX-Request-Id 便于网关鉴权与链路追踪;响应阶段把 { code, data, msg } 解包为 data,非零 code 主动 reject 让业务统一走 catch。与仅处理 HTTP 状态码的库不同,国内很多 REST 接口 HTTP 200 仍表示业务失败,必须在拦截器层转换。

  • Loading 用引用计数:多个并行请求时,只在第一个发出时显示、最后一个结束时隐藏,避免闪烁。
  • 业务码与 HTTP 码分离 :HTTP 200 但 code !== 0 时应在响应拦截器里 reject,否则业务 catch 永远进不去。
  • 401 统一跳转 :比在每个页面 catch 里写 if (status===401) 可维护。
  • 市面应用:Ant Design Pro、Vue Element Admin 的请求封装都是「请求拦截加 Token + 响应拦截解包 + 全局错误提示」同一套路。

【实战要点】

  • 常见坑 :响应拦截器 return response.data 后,外层 then 拿到的是数据而不是完整 response,与未解包时代码不兼容,需团队统一约定。
  • 性能 :拦截器里避免 await 慢接口(如每次请求都拉新 token);Token 刷新应配合 401 重试队列,而不是阻塞所有请求。
  • 取消与拦截器eject(id) 后该槽位变 nullforEach 跳过;热更新环境注意重复 use 导致拦截器叠加。

【面试考点】

Q8:如何实现「同一接口短时间只发最后一次」?

A:在请求拦截器里为 url+params 维护 AbortController(或 CancelToken),新请求 abort 旧请求;或 debounce 搜索框输入。axios v1.6+ 推荐 config.signal

Q9:响应拦截器里 return Promise.reject 和直接 throw 区别?

A:在 async 拦截器里等价;在普通函数里必须 return Promise.reject(err) 才能把链置为 rejected,否则错误被吞掉。


四、总结

4.1 知识点回顾

#mermaid-svg-oFD3HRoQxcGpMF6U{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oFD3HRoQxcGpMF6U .error-icon{fill:#552222;}#mermaid-svg-oFD3HRoQxcGpMF6U .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oFD3HRoQxcGpMF6U .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oFD3HRoQxcGpMF6U .marker.cross{stroke:#333333;}#mermaid-svg-oFD3HRoQxcGpMF6U svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oFD3HRoQxcGpMF6U p{margin:0;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge{stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .section--1 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section--1 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section--1 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section--1 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section--1 text{fill:#ffffff;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth--1{stroke-width:17;}#mermaid-svg-oFD3HRoQxcGpMF6U .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-0 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-0 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-0 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-0 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-0 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-0{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-0{stroke-width:14;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-1 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-1 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-1 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-1 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-1 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-1{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-1{stroke-width:11;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-2 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-2 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-2 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-2 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-2 text{fill:#ffffff;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-2{stroke-width:8;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-3 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-3 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-3 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-3 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-3 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-3{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-3{stroke-width:5;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-4 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-4 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-4 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-4 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-4 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-4{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-4{stroke-width:2;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-5 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-5 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-5 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-5 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-5 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-5{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-5{stroke-width:-1;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-6 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-6 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-6 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-6 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-6 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-6{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-6{stroke-width:-4;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-7 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-7 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-7 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-7 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-7 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-7{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-7{stroke-width:-7;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-8 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-8 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-8 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-8 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-8 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-8{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-8{stroke-width:-10;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-9 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-9 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-9 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-9 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-9 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-9{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-9{stroke-width:-13;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-10 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-10 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-10 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-10 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-10 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-10{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-10{stroke-width:-16;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-root rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-root path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-root circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-root text{fill:#ffffff;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-root span{color:#ffffff;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-2 span{color:#ffffff;}#mermaid-svg-oFD3HRoQxcGpMF6U .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge{fill:none;}#mermaid-svg-oFD3HRoQxcGpMF6U .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-oFD3HRoQxcGpMF6U :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Axios

核心技术
架构设计
函数委托模式
适配器模式
Promise 化
拦截器链
核心功能
请求发送
响应处理
配置合并
超时控制
请求取消
高级特性
拦截器机制
责任链模式
Promise 链
错误处理
实战应用
身份认证
错误统一处理
请求日志
取消重复请求

【代码注释】 此思维导图总结本文的四大知识模块:架构设计 (函数委托、适配器、Promise 化、拦截器链)、核心功能 (请求发送、响应处理、配置合并、超时控制、请求取消)、高级特性 (拦截器机制、责任链模式、Promise 链、错误处理)、实战应用(身份认证、错误统一处理、请求日志、取消重复请求)。这四个层次层层递进,从底层设计到实际应用,构成了完整的 Axios 技术体系。

4.2 高频面试题速查

问题 核心答案
axios 既是函数又是对象? 函数委托模式 + bind + 属性复制
拦截器执行顺序? 请求 LIFO(后进先出),响应 FIFO(先进先出)
请求取消原理? Promise 状态控制 + xhr.abort()
超时机制? xhr.timeout + xhr.ontimeout 事件
跨环境原理? 适配器模式:浏览器用 XHR,Node 用 http
4xx/5xx 如何捕获? xhr.onload 中判断 status,非 200-299 reject
如何统一处理错误? 响应拦截器中根据 error.code 分发错误信息
为何不能简单 Object.assign 合并配置? headers 需深度合并;url/method 以本次请求为准
404 为何走 onload 而非 onerror? HTTP 语义错误在传输层已成功,需在 onload 里 reject
生产拦截器如何分层? Token/traceId → Loading 计数 → 业务码解包 → 401 跳转

4.3 学习建议

  1. 按路线实操 :按 §0.4 八步从「创建函数」做到「拦截器」,每步只改当前版 axios.js 并用 HTML 验证
  2. 对照官方 :下载 axios 官方仓库lib/,与手写版并排,从 axios.jsAxios.jsxhr.js 跟读
  3. 先会用再读源码 :能写 axios.create、拦截器、all 后,再理解 createInstancemergeConfig 的实现差异
  4. 面试准备 :熟记 §4.2 表 + 各章 【面试考点】(函数委托、拦截器顺序、取消原理、404 与 onerror)
  5. 生产演进 :新项目取消优先 AbortController.signal;CancelToken 为兼容旧版 axios

推荐阅读


参考资料

  1. Axios 官方仓库
  2. Axios 官方文档
  3. MDN XMLHttpRequest API
  4. Promise A+ 规范
  5. 责任链模式 - Refactoring Guru
  6. Axios 拦截器实现原理 - 腾讯云
  7. 面试官:你了解axios的原理吗?- Vue3面试题
  8. 最全、最详细Axios源码解读 - 掘金

说明 :文中示例可在本目录保存为 .html 后直接打开;对照学习时可自备 Axios 1.x 官方 lib/ 源码。

适用版本 :Axios 1.x 系列(与 axios 官方仓库 v1.x 分支一致)