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

相关推荐
嘟嘟071718 小时前
useRef + Web Worker:React 中的耗时计算不卡页面
javascript·vue.js
mONESY18 小时前
React Router v7 完全实战指南:从入门到鉴权,一套代码全搞定
javascript
他们叫我秃子18 小时前
前端开发转 Go 全栈(五):终于遇到熟人了,Go 的闭包和高阶函数原来这么像 JavaScript
前端·后端·go
用户615958680002218 小时前
从 0 写一个迷你 wujie:用原生 Web API 实现微前端
前端
妙码生花18 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十七):AI时代对组件封装的新理解、agInput 统一入口,动态表单预备
前端·后端·go
fairyly18 小时前
接手新项目不用硬啃源码,我的 TRAE Work 快速上手实战工作流
前端·ai编程·trae
搬砖记录员18 小时前
用 PyAutoGUI 手搓录屏"老板键":从原理到四套可跑源码,附工程化落地思考
前端
windliang18 小时前
Claude Code 源码分析(七):Skill 如何进入 Agent
前端·人工智能·面试
To_OC18 小时前
LC 74 搜索二维矩阵:换皮的二分查找,我居然一开始没看出来
javascript·算法·leetcode
栀鸢ouo18 小时前
《Vue 3 实现多主题切换和自定义主题》
前端