还在用 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.okres.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.bodyReadableStream

这意味着你可以边接收数据边处理,不需要等整个响应下载完:

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 上踩过哪些坑?欢迎评论区聊聊。

相关推荐
CoderWeen1 小时前
从零实现一个 Vue3 流程图编辑器:节点拖拽、贝塞尔连线与框选
前端·javascript
森鹿1 小时前
express中间件原理以及大致实现
前端·express
光影少年1 小时前
HashRouter 和 BrowserRouter 区别、底层原理、部署差异
前端·react.js·nestjs
柯克七七1 小时前
我把祖传项目的构建时间砍了90%,领导以为我只是在"优化了一下",结果隔壁组的CI都崩了来问我配置
前端·webpack
风骏时光牛马1 小时前
JSP页面直接输出实体对象空属性引发页面500报错实战案例
前端
IT_陈寒2 小时前
Python里这个赋值坑,连老司机都能翻车
前端·人工智能·后端
Hyyy3 小时前
什么是bun?和pnpm有什么区别
前端·面试·bun
To_OC12 小时前
LC 128 最长连续序列:别上来就排序,O (n) 解法才是这题的灵魂
javascript·算法·leetcode
葫芦和十三12 小时前
图解 MongoDB 14|Cache 与淘汰:WiredTiger 的内存治理
后端·mongodb·面试