前后端的数据交互是项目开发过程中不可或缺的关键环节。在这个过程中,XHR 和 Fetch API 作为两种最常见的type,实现从 Web 服务器获取数据。XHR 是传统的数据请求方式,而 Fetch API 则代表了现代 Web 开发的新兴标准。一个诞生于 2000 年,一个发布于 2015 年。它们都是发请求的工具,却有很大的不同。
XHR 的写法:
javascript
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(JSON.parse(xhr.responseText));
}
};
xhr.send();
Fetch 的写法:
javascript
fetch('/api/users')
.then(res => res.json())
.then(data => console.log(data));
同样是发一个 GET 请求,XHR 用了 7 行,Fetch 用了 3 行,这不只是"语法糖"的区别。

一、事件驱动 vs Promise
这是两者最根本的差异。
XHR 是事件驱动的 。它的工作流基于 onreadystatechange 事件,你需要监听 readyState 的变化来判断请求到了哪个阶段。成功、失败、超时、取消,都靠不同的事件回调来处理。这种模式在 2000 年很先进,但在今天看来,它容易导致"回调地狱"------请求一多,代码就层层嵌套。
Fetch 是 Promise 驱动的 。它原生返回 Promise,天然支持 then 链式调用和 async/await。你可以把请求像同步代码一样写:
javascript
const data = await fetch('/api/users').then(res => res.json());
多个请求也可以轻松并发:
javascript
const [users, orders] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/orders').then(r => r.json())
]);
而 XHR 要并发,你需要手动封装 Promise:
javascript
function xhrFetch(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => resolve(JSON.parse(xhr.responseText));
xhr.onerror = reject;
xhr.send();
});
}
Fetch 的 Promise 原生支持,让它天然适配现代 JavaScript 的异步模型。XHR 的事件回调模式是旧时代的产物,虽然也能封装成 Promise。
二、错误处理:Fetch 为什么不"报错"?
这是 Fetch 最反直觉的设计,也是面试高频题。
XHR 的错误处理 :onerror 在请求失败时触发,status 可以判断 HTTP 状态码。
javascript
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// 成功
} else {
// HTTP 错误
}
};
xhr.onerror = function() {
// 网络错误
};
Fetch 的错误处理 :只有网络故障、跨域失败等导致请求无法完成时,Promise 才会 reject。HTTP 404、500 在 Fetch 眼中是"成功的响应"------服务器确实给你回复了,只是内容不是你想要的。
javascript
fetch('/api/404')
.then(res => res.json()) // 这里不会报错!
.catch(err => console.log(err)); // 404 走不到这里
你必须手动检查 res.ok 或 res.status:
javascript
fetch('/api/data')
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
});
Fetch 把 HTTP 状态码看作"业务逻辑"而非"网络错误"。它的职责是帮你拿到响应,至于这个响应的状态码代表成功还是失败,由你的业务代码决定。这种设计更底层,但日常开发中确实不如 XHR 直观。
三、超时与取消
XHR 的超时:属性直接设置。
javascript
xhr.timeout = 5000;
xhr.ontimeout = () => console.log('请求超时');
Fetch 没有 timeout 属性 。你需要用 AbortController + setTimeout 模拟:
javascript
function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
return fetch(url, { signal: controller.signal })
.finally(() => clearTimeout(timer));
}
XHR 的取消 :调用 xhr.abort(),触发 onabort 事件。
Fetch 的取消 :使用 AbortController。
javascript
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal });
controller.abort(); // 取消请求
AbortController 的优势在于:一个信号可以同时取消多个请求。
javascript
const controller = new AbortController();
Promise.all([
fetch('/api/a', { signal: controller.signal }),
fetch('/api/b', { signal: controller.signal }),
fetch('/api/c', { signal: controller.signal }),
]);
controller.abort(); // 三个请求一起取消
这在页面切换时特别有用------一个 abort() 清理所有进行中的请求,而 XHR 需要一个一个调 abort()。React 的 useEffect 清理函数中,这也是标准做法:
jsx
useEffect(() => {
const controller = new AbortController();
fetch(`/api/suggest?q=${keyword}`, { signal: controller.signal })
.then(res => res.json())
.then(setData);
return () => controller.abort();
}, [keyword]);
四、请求进度
如果你需要展示文件上传的进度条,XHR 在这方面完胜 Fetch。
XHR 的上传进度监听:
javascript
xhr.upload.addEventListener('progress', (e) => {
const percent = Math.round(e.loaded / e.total * 100);
console.log(`上传进度:${percent}%`);
});
Fetch 原生不支持进度事件 。你只能手动读取 ReadableStream 来计算,代码量很大:
javascript
// 需要手动包装成流,监听每个 chunk 的大小
const response = await fetch('/upload', { method: 'POST', body: fileStream });
const reader = response.body.getReader();
// ...手动计算
这就是为什么即使 Fetch 已经普及,很多项目在处理文件上传时还是会回到 XHR 或 基于XHR封装的Axios。
五、响应类型与流式处理
XHR 的 responseType:
javascript
xhr.responseType = 'json'; // 或 'text'、'blob'、'arraybuffer'、'document'
Fetch 的等价方法:
XHR responseType |
Fetch 方法 |
|---|---|
'json' |
response.json() |
'text' |
response.text() |
'blob' |
response.blob() |
'arraybuffer' |
response.arrayBuffer() |
'document' |
不支持 |
看起来 Fetch 只是把属性变成了方法,但 Fetch 有一个 XHR 难以匹敌的能力------response.body 是 ReadableStream。
这意味着你可以边接收数据边处理,不需要等整个响应下载完:
javascript
const response = await fetch('/api/chat', { method: 'POST', body: prompt });
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value, { stream: true });
appendToChat(text); // 逐字显示 AI 回复
}
这在 AI 流式输出、大文件分块下载等场景中至关重要。XHR 虽然也支持
responseType = 'stream',但兼容性和灵活性远不如 Fetch。
六、Cookie 携带与安全默认值
XHR 默认携带同源 Cookie。
Fetch 默认不携带任何 Cookie,必须显式设置:
javascript
fetch('/api/user', { credentials: 'include' });
三个选项:
| 值 | 含义 |
|---|---|
omit(默认) |
不发送 Cookie |
same-origin |
仅同源发送 |
include |
同源和跨域都发送 |
Fetch 默认不携带 Cookie 是出于安全考虑。CSRF 攻击利用的就是跨站请求自动带上用户 Cookie。Fetch 的默认值是"最小权限原则"------你需要 Cookie,就显式声明,框架不会帮你"默认带上"。
七、同步请求
XHR 支持同步请求:
javascript
xhr.open('GET', '/api/data', false); // 第三个参数 false = 同步
xhr.send();
// 这行代码执行时,整个页面会卡住,直到请求完成
Fetch 不支持同步请求,这是刻意为之。 同步请求会阻塞主线程,导致页面无响应。现代浏览器主线程不应被阻塞,所以 Fetch 完全不提供同步选项。
八、Node.js 支持
- XHR 只能在浏览器端使用 ,Node.js 端需要用
http模块或第三方库(如axios)。 - Fetch 从 Node.js 18(2022 年)开始原生支持,现在前后端可以用同一套 API 发请求。
这意味着:Fetch 是 JavaScript 生态中真正"同构"的网络请求 API。
十、总结
现代前端项目已经很少直接使用 XHR。Fetch 的 Promise 原生支持、流式处理能力、AbortController 统一取消机制,以及前后端统一的同构能力,让它更符合现代 JavaScript 的理念。
如果你今天开一个新项目,用 Fetch 或基于 Fetch 的封装库已经足够;如果你的项目已经有 Axios 且跑得好好的,完全不需要迁移。理解两者的区别,不是为了"用哪个更好"的争论,而是让你在遇到网络请求相关的 Bug 时,知道该从哪个角度排查。
你在用什么网络请求工具?或者在 Fetch 上踩过哪些坑?欢迎评论区聊聊。