还在用 Axios?你可能需要重新理解 XHR 与 Fetch

前后端的数据交互是项目开发过程中不可或缺的关键环节。在这个过程中,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 上踩过哪些坑?欢迎评论区聊聊。

相关推荐
程序员大阳2 小时前
副队长JavaScript教程(49)-主动抛出异常与错误类型
javascript·异常处理·error·throw·错误类型
hhhjhl2 小时前
Protobuf.js 跨端三方库仓颉适配全流程
javascript·网络·udp
康小汪2 小时前
AI 写 Vue 总写成同一个味道:给它装 Skills,装的不是知识是边界
前端·vue.js·人工智能·ai编程·最佳实践
智能运维指南3 小时前
研制过程“两张皮“怎么破:研制一体化支撑平台的定位、能力边界与落地前提
java·前端·人工智能·嘉为蓝鲸
白山编程大哥4 小时前
Java 后端如何优雅地把 List 集合返回给前端
java·前端·list
Flynt5 小时前
我把热榜那篇"禁用 axios"逐条实测了一遍:结论大体对,坑在它没讲的地方
前端·javascript·axios
考研保研资料分享5 小时前
四非会计保研中国矿大(北京):七场考核、候补与择校复盘
面试
Csvn6 小时前
bundle优化
前端
slongzhang_6 小时前
linux 递归删除源码文件|递归删除目录下 *.js,但排除 *.min.js
linux·开发语言·javascript
北冥有鱼6 小时前
发版后动态路由点击报 `Failed to fetch dynamically imported module`」问题上的完整排查与修复过程
前端·vue.js