Ajax、XHR、Fetch、Axios:你以为你懂,其实可能搞混了
这几个概念看着眼熟,可真要讲清楚区别,很多人就会越说越乱,最后把自己都绕进去了。

上图用于帮你理清四者的关系和时间线。接下来我们展开聊聊------很多人被问到"Ajax和XHR有什么区别时"会愣住,不是因为不懂,而是从来没想过Ajax压根不是一个API。
一、先解释一个最常见的误解
很多人以为 Ajax 是一个像 fetch() 那样可以直接调用的函数,或者是一个浏览器内置对象,但其实并不是这个样子。
Ajax(Asynchronous JavaScript and XML)本质上是一种编程模式(或者说编码理念),而不是某个具体的技术、协议或 API。 它的核心思想是"在不刷新整个页面的情况下,与服务器交换数据并更新部分页面"。它诞生于 2005 年,Jesse James Garrett 在一篇文章里给它起了这个名字。真正让 Ajax 火出圈的是 Google Maps 和 Gmail------用户发现网页居然可以像桌面应用一样流畅交互------这在今天看来稀松平常,但在当时确实是跨时代的进步。
Ajax 本身并没有提供任何 API 和 具体实现,它的底层实现,靠的是浏览器内置的 XMLHttpRequest 对象(简称 XHR)。
一句话:Ajax 是理念,XHR 是实现------前者回答"要做什么",后者回答"怎么做到"。
这时候可能有人会问:那 Fetch 和 Axios 呢?它们不也是做异步请求的吗?和 Ajax 是什么关系?
答案是:Fetch 和 Axios,本质上都是实现 Ajax 这套理念的"工具"------和 XHR 是同一层级的东西,只是走的路子不一样。Fetch 是浏览器原生提供的另一种底层通信方式,用来替代 XHR;Axios 则是一个第三方 HTTP 客户端库,在浏览器环境下,它内部其实也是靠 XHR 来发请求的。
二、为什么有了 XHR,还要有 Fetch?有了 Fetch,还要有 Axios?
技术演进从来不是"后出现的全面碾压前一个",而是新方案解决了旧方案的痛点,但也带来了新的问题。
2.1 XHR:功高盖主,但写法劝退
XHR 最早是 1999 年微软在 IE5 里以 ActiveX 控件的形式引入的,目的是给 Outlook Web Access 做异步数据交互。后来 W3C 把它标准化,所有主流浏览器都支持了。
它的经典写法长这样:
javascript
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/user', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log(JSON.parse(xhr.responseText));
} else {
console.error('出错了', xhr.statusText);
}
}
};
xhr.send();
readyState 有 5 个状态(0~4),onreadystatechange 事件会在每次状态变化时触发。这种"事件驱动 + 状态机"的模式在 2000 年代很合理,但放到 Promise 和 async/await 普及的今天,就显得有点啰嗦了。
而且 XHR 的 API 设计也不太符合现代开发习惯------配置、发送、监听、错误处理都集中在一个对象上。
一句话:XHR 是浏览器网络请求的"万能插座",但插上去的过程有点繁琐。
2.2 Fetch:Promise 时代的原生答案
2015 年,ES6 带来了 Promise,浏览器也顺势推出了 Fetch API。它的设计目标很简单:提供一个基于 Promise 的、更现代的原生 HTTP 请求 API。
最简版本:
javascript
fetch('https://api.example.com/user')
.then(res => res.json())
.then(data => console.log(data));
配合 async/await 后更加清爽:
javascript
const res = await fetch('https://api.example.com/user');
const data = await res.json();
看起来很美,对吧?但事情没那么简单。
Fetch 有几个"坑"是很多人踩过的:
- HTTP 错误不会自动进 catch :404 或 500 返回的 Promise 依然通常是 fulfilled,你需要手动判断
res.ok。 - 跨源 Cookie 默认不会随请求发送 :跨源场景通常需要显式设置
credentials: 'include'。 - 超时需要结合取消机制实现 :现代 Fetch 通常使用
AbortController/AbortSignal处理超时和取消。 - 进度监控相对复杂 :XHR 有
onprogress等事件,Fetch 则通常需要结合ReadableStream自己读取数据并计算进度。 - IE 完全不支持:虽然 IE 已经退出历史舞台,但当年这是一个实打实的兼容性问题。
所以 Fetch 更像是一个"底层、通用、现代"的原语,而不是"开箱即用"的完整 HTTP 客户端。
一句话:Fetch 给了你一个更优雅的起点,但走到生产环境,你会发现一些工程能力需要自己补齐。
2.3 Axios:不是 Fetch 的升级版,而是另一条路
Axios 诞生于 2014 年,真正火起来是在 Vue/React 生态爆发之后。这里有个关键认知:
Axios 不是 Fetch 的"高级封装",它和 Fetch 是平行关系。
| 维度 | Fetch | Axios |
|---|---|---|
| 本质 | 浏览器原生标准 API | 第三方 HTTP 客户端库 |
| 浏览器端底层 | 浏览器原生 Fetch 实现 | 主要使用 XHR,也可使用其他适配器 |
| Node.js | 现代 Node.js 已原生提供 Fetch | 可使用 Node.js HTTP 等适配器 |
| Promise | 原生支持 | 原生支持 |
| 超时控制 | 结合 AbortSignal 等机制 | 提供 timeout 等配置 |
| 拦截器 | 没有传统意义上的内置拦截器 | 内置请求/响应拦截器 |
| 自动 JSON 转换 | 需要调用 res.json() 等方法 |
自动转换常见 JSON 响应 |
| HTTP 错误处理 | 默认不因 4xx/5xx 自动 reject | 默认可按状态码策略 reject |
| 请求取消 | AbortController / AbortSignal |
支持 AbortController |
| 浏览器 + Node.js | 现代运行环境均可使用 | 浏览器 + Node.js |
Axios 的核心价值在于:它把日常开发中的大量重复劳动(设置 baseURL、加请求头、错误统一处理、JSON 转换、请求取消等)通过统一 API 和拦截器机制封装起来。
javascript
// 创建一个 axios 实例,全局配置一次
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000,
headers: { 'X-Custom-Header': 'foobar' }
});
// 请求拦截器:每次发请求前自动加 token
api.interceptors.request.use(config => {
config.headers.Authorization =
`Bearer ${localStorage.getItem('token')}`;
return config;
});
// 响应拦截器:统一处理错误
api.interceptors.response.use(
res => res.data,
err => {
if (err.response?.status === 401) {
// 统一跳转登录
}
return Promise.reject(err);
}
);
// 使用时简洁到离谱
const user = await api.get('/user');
一句话:Axios 是一个"为开发者体验优化"的 HTTP 客户端,它不是任何原生 API 的替代品,而是工程化的请求工具。
三、底层原理:它们到底怎么把请求发出去的?
聊完"为什么有它们",我们再往下探一层------在浏览器里,当这些 API 发起请求后,酒精发生了些什么?
3.1 XHR 的完整链路
当你写下 xhr.send() 时,浏览器内部大致经历这样一套流程:
text
JS 执行环境
↓ 创建 XMLHttpRequest 对象
↓ 配置请求参数 (open)
↓ 注册回调 (onreadystatechange / onload / onerror)
↓ send() ------ 将请求交给浏览器网络栈
浏览器网络层
↓ DNS 解析
↓ 建立或复用网络连接
↓ 发送 HTTP 请求
↓ 等待服务端响应
↓ 接收响应数据(响应头 → 响应体)
浏览器
↓ 将网络事件/任务调度回 JavaScript 执行环境
↓ 事件循环调度
↓ 执行你的 onreadystatechange / onload 回调
需要注意:现代浏览器的进程模型和网络实现比上面的简化流程复杂得多,不能简单理解成固定的"JS 主线程 → 网络进程 → IPC → 主线程"单一路径。
XHR 的 readyState 五个状态,本质上反映了请求对象所处的生命周期阶段:
| readyState | 含义 | 阶段 |
|---|---|---|
| 0 | UNSENT | 对象刚创建,open() 还没调用 |
| 1 | OPENED | open() 已调用,请求已配置 |
| 2 | HEADERS_RECEIVED | 已收到响应头 |
| 3 | LOADING | 正在接收响应体 |
| 4 | DONE | 请求完成 |
这也是为什么 XHR 能提供比较直接的进度监控 (如 onprogress)和同步模式(虽然同步 XHR 会阻塞执行环境,现代 Web 开发通常不应该使用)。
3.2 Fetch 的底层有什么不同?
Fetch API 并不是"在 XHR 外面套了一层 Promise"。从 Web 平台标准和浏览器实现角度看,Fetch 与 XHR 是不同的 API 抽象,它们最终都可以进入浏览器的网络栈,但并不存在一个简单的"Fetch → XHR"固定调用关系。
Fetch 的设计和 Request / Response / Headers / Streams / Service Worker 等 Web 平台能力紧密相关。
它的请求流程可以粗略理解为:
text
fetch(url)
↓ 创建/规范化 Request
↓ 按 Fetch 标准执行请求流程
↓ Service Worker(如果满足条件并被拦截)
↓ 浏览器网络层
↓ Promise 得到 Response
↓ Response.body 可以作为 ReadableStream 使用
↓ 调用 .json() / .text() 等方法消费响应体
关键区别在于:Fetch 返回的 Response 并不意味着响应体已经全部读取完成。响应头到达后,就可以得到 Response,之后再通过 response.body 或 .json()、.text() 等方式消费响应体。
这也是 Streams API 的重要意义之一:应用可以逐步处理响应数据,而不是只能等待整个响应体一次性拿到。
而 Fetch 的取消机制通常使用 AbortController / AbortSignal:
javascript
const controller = new AbortController();
const response = await fetch('/api/user', {
signal: controller.signal
});
// 取消请求
controller.abort();
3.3 Axios 的底层:它到底调了什么?
Axios 在浏览器端的经典实现路径主要是 XHR;具体底层适配器会随 Axios 版本和运行环境变化,因此不应该简单概括成"Axios 永远就是 XHR"。
在浏览器中,可以把它理解成:
text
你的业务代码
↓ 调用 axios.get('/api')
Axios
↓ 合并配置(baseURL、headers、params 等)
↓ 执行请求拦截器
↓ 选择浏览器端请求适配器
↓ 浏览器环境下通常使用 XHR
↓ 接收响应
↓ 执行响应拦截器
↓ 返回 AxiosResponse / 业务数据
而在 Node.js 环境中,Axios 会根据适配器使用 Node.js 对应的网络能力。
这就是为什么 Axios 能够在浏览器和 Node.js 等不同运行环境中提供相对统一的调用方式。
一句话总结这一节:XHR 和 Fetch 是不同的 Web 平台请求 API;Axios 则是在这些底层能力之上提供统一配置、拦截器、转换和错误处理等工程化能力的 HTTP 客户端。
四、实际项目中,到底怎么选?
聊到这里,你可能已经有点感觉了。但在实际项目中如果只是无脑使用 Axios 是不够的,具体的选择取决于场景。
场景 1:写一个极简的 Demo 或脚本
javascript
const data = await (await fetch('/api/ping')).json();
Fetch 的原生、无额外依赖在这里就是优势。
场景 2:企业级前端项目(Vue/React/Angular)
可以考虑 Axios,也可以直接基于 Fetch 封装一层自己的请求客户端。
选择 Axios 的常见原因:
- 拦截器:统一加 token、统一处理 401,不用在每个组件里重复写。
- 超时控制:统一处理请求超时。
- 自动 JSON 转换:减少重复的响应解析代码。
- 请求取消 :结合
AbortController管理不再需要的请求。 - 统一实例配置 :集中管理
baseURL、headers、参数序列化等。
但也不要把 Axios 当成"企业项目必选项"。现代前端完全可以基于 Fetch 封装出一套满足项目需求的请求层。
场景 3:需要上传/下载进度条
XHR 原生支持 onprogress、上传进度等能力,Axios 也可以暴露相应能力:
javascript
axios.get('/large-file', {
onDownloadProgress: (e) => {
if (e.total) {
const percent = Math.round((e.loaded / e.total) * 100);
console.log(`下载进度: ${percent}%`);
}
}
});
Fetch 同样可以做,但通常需要读取 ReadableStream 并自行计算进度,代码和处理复杂度会更高。
场景 4:兼容老旧环境
如果项目还要支持 IE11,Fetch 原生不可用。这时候可以使用 XHR,或者选择能够兼容目标环境的第三方方案。
需要注意的是,不能简单说"Axios 自动兼容所有老浏览器"。实际兼容性还取决于 Axios 版本、构建目标以及运行环境。
场景 5:学习或面试
手写一个 Ajax 请求是经典面试题。这时候你需要理解 XHR 的完整生命周期------open、send、readyState、status、responseText,一个都不能少。
javascript
function myAjax(url, method = 'GET', data = null) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(new Error(xhr.statusText));
}
}
};
xhr.onerror = () => reject(new Error('网络请求失败'));
xhr.send(data);
});
}
一句话:Fetch 适合"轻量、原生、无额外依赖"的场景;Axios 适合"工程化、统一封装、功能丰富"的场景;XHR 更适合学习底层机制、兼容特殊环境以及某些需要直接使用 XHR 能力的场景。
五、一张图 + 几句话,带走核心认知
如果你看完只记得几件事,那就记住这些:
| 概念 | 它到底是什么 | 一句话定位 |
|---|---|---|
| Ajax | 技术方案/编程模式 | "异步局部更新页面"的思想,不是某个具体 API |
| XHR | 浏览器 Web API | 经典的异步网络请求 API,事件驱动,生命周期状态明显 |
| Fetch | 浏览器 Web API | 现代标准网络请求 API,基于 Promise,并与 Streams 等能力结合 |
| Axios | 第三方 HTTP 客户端库 | 对底层请求能力进行工程化封装,提供拦截器、统一配置等能力 |
它们的关系不是简单的"谁替代谁",而是处于不同层次:
- Ajax 是一种开发思想/模式
- XHR 和 Fetch 是浏览器提供的两套请求 API
- Axios 是第三方 HTTP 客户端库
最后送一个面试小技巧:如果面试官问"Fetch 和 Axios 有什么区别",不要说"Axios 是 Fetch 的高级版"------这是不准确的。
更好的回答是:
"Fetch 是浏览器原生的标准 Web API,设计偏底层和通用;Axios 是第三方 HTTP 客户端库,在浏览器端主要基于 XHR,并提供了统一配置、拦截器、转换和错误处理等工程化能力。两者不是简单的上下级关系,也不存在 Axios 就是 Fetch 封装这一说。"
这篇文章如果对你有帮助,欢迎点赞收藏。你在项目里更常用 Fetch 还是 Axios?遇到过什么坑?评论区聊聊。