先记住这个答案
核心差异有四点:第一,fetch 返回 Promise,可配合 async/await 组织链式逻辑,XHR 依赖 onload、onerror 等事件回调。第二,fetch 收到 HTTP 404 或 500 时 Promise 依然 resolve,只有网络层失败才 reject,必须检查 response.ok;XHR 可通过 status 在回调中统一判断。第三,XHR 的 upload 对象支持上传进度事件,原生 fetch 无法监听上传进度。第四,fetch 用 AbortController 取消,XHR 用自身的 abort() 方法。选型上:需要上传进度或兼容极老环境用 XHR,其余场景优先 fetch 并自行封装错误检查与超时。
- fetch 只在网络失败时 reject,HTTP 错误照常 resolve
- XHR 支持上传进度,原生 fetch 做不到
- fetch 用 AbortController 取消,XHR 用 abort()
- 无特殊需求优先 fetch,但需封装 ok 检查
四个实质差异的机制来源
fetch 的设计把网络层与 HTTP 层分开:Promise 在收到响应头那一刻就 resolve,哪怕状态码是 500,因为传输本身成功了;只有 DNS 失败、连接中断、CORS 阻断这类网络层问题才 reject。XHR 则把一切都交给事件,onload 后由开发者读 status 判断,语义上反而更直白。
上传进度差异源于 API 结构:XHR 把上传通道暴露为 xhr.upload 对象并派发 progress 事件,而 fetch 的 Response 流只覆盖下载方向;流式请求体上传在 Chrome 105+ 才出现且需 duplex: 'half',仍无进度事件。取消方面,fetch 把取消信号外置为 AbortController,可复用到多个请求;XHR 的 abort() 是实例方法,终止后状态会被重置为未发送(readyState 0),同一对象可再次 open() 复用。
大文件上传与接口请求共存的选型
一个后台系统有两类请求:普通 JSON 接口,以及最大 2GB 的视频素材上传,产品要求上传时显示百分比进度条并允许中途取消。决策是:JSON 接口统一走 fetch,封装检查 response.ok、非 ok 抛出自定义错误;视频上传走 XHR,把 xhr.upload.onprogress 的 loaded/total 转成进度条,取消按钮调用 xhr.abort()。
这样处理的原因是硬约束驱动而非偏好:fetch 原生拿不到上传进度,若强行用 fetch 只能等结束后一次性反馈,2GB 文件下用户体验不可接受;而 JSON 接口用 XHR 则要手写回调包装,错误分支分散。结果是一套双轨封装,进度准确到分块,取消两种机制各自落地。
容易失效的假设与应对
最常见的失效假设是 fetch 的 catch 能捕获后端错误:实际上 404、500 都会进入 then,漏掉 ok 检查会把错误响应当成功数据解析,下游 response.json() 再抛解析错误,定位被严重误导。另一个边界是 fetch 没有 timeout 参数,慢请求会无限挂起,必须用 AbortController 加计时器或 AbortSignal.timeout() 自行实现。
对应处理都有代价:封装统一的 fetch 拦截器检查 ok 并抛出带 status 的错误,是所有调用方都要遵守的约定;XHR 的 timeout 属性虽内置,但只在整体超时维度工作;同步 XHR 已被废弃,不应再使用。取消请求时还要注意区分 AbortError 与真实网络错误,否则会把用户主动取消误报为故障告警。
容易答错的地方
- fetch 状态码 500 会进 catch
- 错误。fetch 只在网络层失败时 reject,500 会正常 resolve。必须在 then 中检查 response.ok 或 status,否则错误响应会被当作成功结果继续处理。
- fetch 全面优于 XHR,可以彻底替代
- 不准确。原生 fetch 不支持上传进度,没有内置 timeout,旧环境兼容性也不如 XHR。在需要上传进度条的场景,XHR 仍是更简单直接的方案。
面试官还会怎么问?
fetch 如何正确取消一个请求?
创建 AbortController,把 controller.signal 传给 fetch,调用 controller.abort() 后 Promise 以 AbortError reject,浏览器同时关闭底层连接。需在 catch 中用 error.name === 'AbortError' 区分主动取消与网络失败。
为什么 XHR 能监听上传进度而 fetch 不能?
XHR 把上传通道暴露为 xhr.upload 对象并派发 progress 事件,而 fetch 的流能力主要覆盖响应下载方向。流式请求体上传需 Chrome 105+ 且设置 duplex: 'half',但依然不提供进度回调。
封装 fetch 时最应统一处理什么?
至少三件事:检查 response.ok 并抛出带 status 的错误、用 AbortSignal.timeout() 提供超时、集中处理 JSON 解析失败。否则每个调用方各自处理,容易漏掉 HTTP 错误分支导致隐性 bug。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。