这三个词几乎天天在面试题和文档里一起出现,很多人却把它们当成三个并列的东西,混着用。其实它们根本不在一个层级:
AJAX = 一种「不刷新页面就能请求数据」的思想
XHR = 实现这个思想的第一个 API(XMLHttpRequest)
Fetch = 实现这个思想的第二个 API(fetch)
AJAX 是概念,XHR 和 Fetch 是它的两代实现。 把这句话记住,剩下都是细节。
一、AJAX:它压根不是一个 API
AJAX 全称 Asynchronous JavaScript and XML,是 2005 年 Jesse James Garrett 在一篇文章里造出来的词,用来形容 Gmail、Google Maps 展示的那种体验:页面不重新加载,就能偷偷跟服务器换数据。
所以 AJAX 描述的是一种做法,不是一个能 new 出来的对象:
用户操作 → JS 在后台发请求 → 拿回数据 → 只更新页面的一小块
(整个过程页面不刷新)
有个历史遗留点值得说:名字里的 X(XML) 早就名不副实了。今天 AJAX 传的数据 99% 是 JSON,但这个词照用。所以你现在看到「AJAX 请求」,把它理解成「前端异步请求」就行,它不特指任何一个 API。
二、XHR:第一代实现
XHR,全称 XMLHttpRequest。它才是真正干活的第一个浏览器 API——1999 年随 IE5 出现,后来被标准化。AJAX 那篇文章能火,靠的就是它。
XHR 是事件驱动 + 回调的风格:
const xhr = new XMLHttpRequest()
xhr.open('GET', '/api/user')
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) { // 4 = 请求完成
if (xhr.status >= 200 && xhr.status < 300) {
console.log(JSON.parse(xhr.responseText))
}
}
}
xhr.timeout = 5000 // 原生支持超时
xhr.onprogress = e => console.log(e.loaded, e.total) // 原生支持进度
xhr.onerror = () => console.error('网络错误')
xhr.abort() // 原生支持取消
xhr.send()
它的特点是啰嗦:readyState、回调嵌套、手动解析 JSON,一层套一层就成「回调地狱」。
三、Fetch:第二代实现
Fetch 是 Promise 风格的现代 API,属于 WHATWG 的 Fetch 标准。写法干净得多:
async function getUser() {
const res = await fetch('/api/user')
if (!res.ok) throw new Error(`HTTP ${res.status}`) // 必须手动判!见下文
const data = await res.json()
return data
}
await 一行搞定,天然配合 async/await,还支持流式读取响应体。看起来是全面升级。
但——Fetch 不是 XHR 的严格超集,它砍掉了一些能力,这也是它最大的坑。
四、核心差异表
| 维度 | XHR | Fetch |
|---|---|---|
| 出现时间 | 1999 | 2015 左右 |
| 编程模型 | 事件 + 回调 | Promise / async-await |
| 请求失败时 | onerror(仅网络错误) | Promise reject(仅网络错误) |
| HTTP 4xx/5xx | 需手动查 status | 也 resolve,需手动查 res.ok |
| 超时 | 原生 xhr.timeout | 无,要 AbortController |
| 上传进度 | 原生 onprogress | 无标准方案 |
| 下载进度 | 原生 onprogress | 需读 response.body 流 |
| 取消请求 | xhr.abort() | AbortController |
| 流式响应 | 不支持 | 支持 ReadableStream |
| 同源带 Cookie | 默认带 | 默认带(credentials: 'same-origin') |
| 跨域带 Cookie | withCredentials = true | credentials: 'include' |
最需要记的一条:Fetch 遇到 404、500 照样 resolve,不会进 catch。 它只在网络层失败(断网、DNS 错、被 CORS 拦)时才 reject。所以 res.ok 那句判断绝对不能省,新手翻车多半翻在这。
五、Fetch 的坑:为什么很多人还在用 XHR
Fetch 看着先进,但这几个短板让它在某些场景反而不如 XHR:
- 不 reject HTTP 错误:400/500 都得自己判
res.ok,容易漏。 - 没有超时:得自己配
AbortController+setTimeout,写起来一点都不优雅。
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 5000)
try {
const res = await fetch('/api/slow', { signal: controller.signal })
} finally {
clearTimeout(timer)
}
- 没有上传进度:传大文件想看进度条,Fetch 目前没有标准方案,XHR 的
onprogress才是现成的。 - 老环境不认:极老的浏览器、某些旧 Node 版本需要 polyfill。
六、别把 axios 也算进来
很多人把 axios 当成「第三种方案」,其实它是XHR 的封装库(浏览器端底层就是 XHR,Node 端是 http 模块)。它不改变 AJAX 的本质,只是补齐了 Fetch 缺的东西:
axios = XHR + Promise + 超时 + 拦截器 + 自动 JSON + 统一错误处理
拦截器(统一加 token、统一处理 401)是它的杀手锏,Fetch 想做到同样效果得自己包一层。
七、那到底怎么选
新项目、普通接口请求 → fetch(原生、够轻)
需要上传进度 / 拦截器 / 老兼容 → axios
维护老代码 / 需要 onprogress → XHR
只想改名、其实没换实现 → 警惕:axios 底层还是 XHR
一句话:能用 Fetch 就用 Fetch,需要它没有的能力再上 axios,裸写 XHR 基本只剩维护老代码这一种理由。
小结
- AJAX 是思想(不刷新页面异步取数据),2005 年造的词,名字里的 XML 已经过时。
- XHR 是第一代实现:事件回调风格,啰嗦,但原生带超时、进度、取消。
- Fetch 是第二代实现:Promise 风格,简洁、支持流式,但不带超时、上传进度,且 4xx/5xx 不会 reject。
- axios 不是第三种方案,它是 XHR 的封装。
- 层级关系:AJAX ⊃ { XHR, Fetch };前者是概念,后两者是实现。
- 选型口诀:普通请求用 Fetch,要进度/拦截器用 axios,裸 XHR 多是遗产代码。