fetch 默认不抛出 HTTP 错误状态码(如401),需手动检查 response.ok;XMLHttpRequest 则始终触发 onload,须自行判断 xhr.status;fetch 默认不带 Cookie,需显式设置 credentials: 'include';上传进度仅 XMLHttpRequest 原生支持;AbortController 提供可共享的取消信号,而 xhr.abort() 粗暴终止连接。

XMLHttpRequest 和 fetch 都能发 HTTP 请求,但选错一个,轻则登录态失效、进度条做不出来,重则在 Safari 里白屏、在 IE 里直接报错——不是语法写错了,是底层行为根本不同。
为什么 fetch 会静默忽略 401 错误?
因为 fetch 只在真正网络失败(如断网、DNS 解析失败、CORS 被拒)时 reject Promise;而 401、404、500 这类 HTTP 状态码,它照常 resolve,返回一个 Response 对象。XMLHttpRequest 则相反:只要请求发出去了(哪怕服务端返回 500),onload 仍会触发,必须手动检查 xhr.status 才知道是不是业务失败。
常见后果:
- 你写了
fetch('/api/profile').then(r => r.json()),结果 401 时控制台没报错,但r.json()报错或解析出空对象 - 你没加
if (!response.ok) throw new Error(...),就等于默认接受所有 HTTP 状态码为“成功” -
XMLHttpRequest的onerror只捕获连接级异常,漏掉 4xx/5xx 更隐蔽——但至少你得主动写判断,不容易忘
credentials 默认行为不一致,导致登录态突然丢失
同域下,XMLHttpRequest 默认携带 Cookie;fetch 默认不带,等价于 { credentials: 'omit' }。这意味着:你在 Vue 或 React 里把 axios 换成原生 fetch,几乎必然遇到 401。
修复方式很明确:
立即学习“前端免费学习笔记(深入)”;
- 需要 Cookie 时,
fetch(url, { credentials: 'include' }) - 跨域时,后端必须响应
Access-Control-Allow-Origin具体域名(不能是*),且必须带Access-Control-Allow-Credentials: true -
XMLHttpRequest对应的是xhr.withCredentials = true,效果等价但写法位置不同
上传大文件时,progress 事件不可替代
要显示上传进度条,XMLHttpRequest 直接支持 xhr.upload.onprogress,能拿到 event.loaded 和 event.total;fetch 原生不提供上传过程的流式暴露,无法得知已发多少字节。
目前可行方案有限:
- 用
XMLHttpRequest—— 最稳,兼容到 IE11 - 用
fetch+ReadableStream分块读取文件再手动拼FormData,再逐块发送并计数——复杂、易出错、Safari 不支持TransformStream - 引入第三方库(如
uploadx)封装,但增加了包体积和维护成本
AbortController 不是语法糖,是信号协调机制
xhr.abort() 是立即终止底层连接,粗暴但有效;AbortController 是发一个可监听的取消信号,fetch 内部响应这个信号。关键区别在于:一个 AbortSignal 可以被多个 fetch 共享,也能传给 setTimeout 或 stream.pipeTo() 协同取消。
但要注意:
- 调用
controller.abort()后,对应fetch的 Promise 立即 reject,错误类型是AbortError -
XMLHttpRequest中止后,onreadystatechange仍可能触发一次(readyState === 0),需额外判断 - 若你封装请求函数,
AbortController更适合统一管理生命周期;但简单场景下,xhr.abort()更直觉
最常被忽略的一点:两者不是渐进替代关系。你不能指望 fetch 在旧版 Safari 上自动降级——它就是不支持。而 XMLHttpRequest 的状态机和事件模型,在调试超时、重试、中断恢复时,反而比 Promise 链更透明。选哪个,取决于你是否真的需要 response.body 流式消费,还是只是想少写两行代码。



















