JS中怎么取消正在进行的Fetch或XMLHttpRequest请求
详细讲解在 JavaScript 中如何使用 AbortController 取消 Fetch 请求,以及如何使用 abort() 方法取消 XMLHttpRequest 请求。深入剖析两者的底层原理、状态机变化、异常捕获规范与工程防坑指南。
signal 信号对象进行解耦中断;对于传统的 XMLHttpRequest (XHR),则直接调用该请求实例自带的 .abort() 方法。两者在调用后都会促使浏览器底层立即掐断网络响应的接收通道并释放并发资源,但在状态机流转、异常捕获形态及多请求复用机制上有着本质的区别。
一、使用 AbortController 取消 Fetch 请求
在早期的 ES6 Promise 设计中,Promise 实例本身被设计为“一旦创建便无法从外部强制决议或撤销”。因此 Fetch API 无法像传统的面向对象 API 那样提供 fetch.cancel() 这种实例方法。现代 Web 规范通过引入通用的 DOM 中断标准——AbortController,实现了请求控制器与网络执行过程的彻底解耦。
1. 标准四步操作流程
要使用 AbortController 取消一个正在进行的 Fetch 请求,操作逻辑非常清晰且固定,主要分为四个步骤:
- 实例化控制器:通过
const controller = new AbortController()创建控制器实例。 - 提取并挂载信号:将
controller.signal作为选项参数中的signal属性传递给fetch(url, { signal })。 - 触发中断指令:在需要终止请求的业务时机(如用户点击“取消”按钮、页面跳出、或新搜索词输入时),调用
controller.abort()。 - 捕获 AbortError:在 Promise 的
catch块中检查错误类型,正确识别取消操作并静默处理。
// 1. 创建 AbortController 实例
const controller = new AbortController();
const { signal } = controller;
// 2. 发起请求时绑定 signal
fetch('https://api.example.com/long-task', {
method: 'GET',
signal: signal
})
.then(response => response.json())
.then(data => {
console.log('请求成功获取数据:', data);
})
.catch(error => {
// 4. 关键:区分正常的主动取消与真正的网络异常
if (error.name === 'AbortError') {
console.log('Fetch 请求已由客户端成功取消');
} else {
console.error('发生真实的网络故障或服务器错误:', error);
}
});
// 3. 在需要中断的时刻触发取消(例如 300 毫秒后用户取消)
setTimeout(() => {
controller.abort();
}, 300);
2. 原生超时中断:AbortSignal.timeout()
在过去的开发中,如果要为 Fetch 增加超时取消功能,通常需要使用 setTimeout 手动调用 controller.abort()。而现代现代浏览器与 Node.js (17.3+) 已经原生支持 AbortSignal.timeout(ms),无需额外创建控制器即可直接实现超时中断:
async function fetchWithNativeTimeout(url) {
try {
// 超过 5 秒未完成则自动触发底层中断
const response = await fetch(url, {
signal: AbortSignal.timeout(5000)
});
return await response.json();
} catch (error) {
if (error.name === 'TimeoutError') {
console.warn('请求已超时(超过 5000ms),连接已主动关闭');
} else {
console.error('请求失败:', error);
}
}
}
AbortController 实例产生的 signal 可以同时传递给多个并行的 fetch 请求。一旦调用该控制器的 abort(),所有侦听此信号的网络请求都会在同一瞬间全部中断。这在单页应用(SPA)中切换页面路由时尤为强大——只需触发一次控制器,即可清空当前视图尚未完成的所有后台请求。
二、使用 .abort() 取消 XMLHttpRequest 请求
与 Fetch 的信号解耦设计不同,XMLHttpRequest (XHR) 是早期的面向对象模型。每一个请求都是一个独立的实例对象,其中断逻辑直接内聚在实例的 .abort() 方法中。
1. XHR 中断调用与状态变迁
只要一个 XHR 请求已经被初始化或发送(即处于 OPENED、HEADERS_RECEIVED 或 LOADING 状态),随时调用 xhr.abort() 都会立刻终结该请求:
// 创建 XMLHttpRequest 实例
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/stream-data', true);
// 监听取消事件与完成事件
xhr.onabort = function() {
console.log('XHR 请求已被 abort() 中断,当前 readyState 为:', xhr.readyState); // 输出 0 (UNSENT)
};
xhr.onerror = function() {
console.error('XHR 网络错误');
};
xhr.onload = function() {
console.log('数据接收完毕:', xhr.responseText);
};
xhr.onloadend = function() {
console.log('无论成功、失败或被取消,loadend 总会最终触发');
};
// 发出请求
xhr.send();
// 模拟在请求处理中途由用户主动取消
document.getElementById('cancel-btn')?.addEventListener('click', () => {
if (xhr.readyState !== XMLHttpRequest.DONE) {
xhr.abort();
}
});
2. 调用 abort() 后的底层连带影响
当执行 xhr.abort() 后,浏览器会按标准执行以下关键行为:
- 属性重置:
xhr.readyState会被重置为0(UNSENT),且status会被重置为0,已经接收的响应体会被清空。 - 事件派发:浏览器会向 XHR 对象派发
abort事件,触发xhr.onabort回调;接着触发loadend事件。值得注意的是,不会触发error或load回调。 - 超时属性:XHR 自带
xhr.timeout毫秒属性与xhr.ontimeout事件回调。若发生超时,浏览器内部本质上也会执行类似于 abort 的底层逻辑。
三、Fetch 与 XMLHttpRequest 取消机制的深层对比
理解 Fetch 与 XHR 在取消机制上的架构差异,有助于我们在封装通用网络请求库(如 Axios、Ky 或自研 SDK)时做出正确的异常设计与资源管理:
| 特性维度 | Fetch (搭配 AbortController) | XMLHttpRequest (xhr.abort) |
|---|---|---|
| 架构模型 | 控制器与信号解耦(符合 DOM AbortSignal 标准规范) | 方法直接内聚在请求实例本身 |
| 结果表达方式 | Promise 决议被 Reject,抛出 AbortError |
触发 DOM 事件(onabort 和 onloadend) |
| 多请求协同 | 原生支持:一个 signal 可同时赋予多个 fetch |
无原生机制:必须在外部用数组维护每个实例并循环调用 |
| 状态可逆性 | 不可逆:signal.aborted 变为 true 后无法重置 |
实例可复用:abort 之后理论上可再次调用 open() |
| 超时配置 | AbortSignal.timeout(ms) 原生信号支持 |
实例属性 xhr.timeout = ms |
四、容易混淆的关键认知与避坑实践
1. 混淆点一:“客户端取消请求”是否等于“服务端停止执行”?
这是前后端交互中最普遍的误区。很多开发者认为只要前端调用了 controller.abort() 或 xhr.abort(),服务器端正在执行的数据库查询或文件写入就会立刻停止。在大多数默认情况下,这是不成立的。
当前端调用取消时,浏览器会在传输层关闭 Socket 连接,并向对端发送 TCP RST 报文(或在 HTTP/2 / HTTP/3 连接中发送 RST_STREAM 帧),然后立即释放前端的内存缓冲区。但如果后端的 HTTP 处理线程(如 Java Spring、Python Django、Node.js Express)在读取完请求体后已经进入了密集的计算或数据库事务,且没有显式监听客户端 Socket 的断开事件,服务端程序仍然会一直运行至该接口逻辑结束。
因此,对于具有高副作用的写操作(如扣款、发帖、生成大文件),前端取消请求主要用于保护前端视图的一致性与避免竞态覆盖;而服务端自身的安全性必须依赖防重令牌(Idempotency Key)与显式的连接关闭监听(例如 Node.js 中的 req.on('close'))。
2. 混淆点二:AbortController 实例的“单向不可逆”陷阱
在实现搜索框输入联想(防抖请求)时,新手常犯的一个典型错误是将 AbortController 实例保存在模块顶层或组件状态中,并在每次输入时重复使用:
// ❌ 错误做法:重复使用已被 abort 的同一个控制器
let globalController = new AbortController();
function searchBad(keyword) {
// 一旦第一次执行 abort(),该实例的 signal.aborted 将永久为 true!
globalController.abort();
// 后续发起的所有 fetch 会在排队时立即被拒绝,导致所有后续搜索永远失败
fetch(`/api/search?q=${keyword}`, { signal: globalController.signal })
.then(...)
}
// 正确做法:每次发起前,终止旧控制器,并立即重新 new 一个新控制器
let activeController = null;
function searchGood(keyword) {
if (activeController) {
activeController.abort(); // 中断上一次尚在路上的请求
}
activeController = new AbortController(); // 创建全新的信号容器
fetch(`/api/search?q=${keyword}`, { signal: activeController.signal })
.then(res => res.json())
.then(data => {
renderList(data);
})
.catch(err => {
if (err.name === 'AbortError') return; // 忽略预期的取消,避免错误弹窗
showToast('搜索服务异常,请重试');
});
}
3. 混淆点三:在 React / Vue 组件卸载中的内存泄漏与竞态防范
在 React 的 useEffect 或 Vue 的组件生命周期中,异步数据返回时组件可能已经被用户关闭或销毁。直接在未卸载检查的情况下设置状态会引发状态错乱与内存泄漏警报。使用 AbortController 是解决此问题的标准范式:
// React 组件内的数据获取与清理模式
useEffect(() => {
const controller = new AbortController();
async function loadUserProfile() {
try {
const res = await fetch(`/api/user/${userId}`, {
signal: controller.signal
});
const data = await res.json();
setUser(data);
} catch (error) {
// 若组件卸载触发了取消,静默忽略即可
if (error.name !== 'AbortError') {
setError(error);
}
}
}
loadUserProfile();
// 组件卸载或 userId 变化时的清理回调
return () => {
controller.abort();
};
}, [userId]);
五、总结与工程选型建议
在当下的 JavaScript 工程开发中,对于网络请求的取消控制,推荐遵循以下准则:
- 优先采用 Fetch + AbortController:作为现代 Web 标准,
AbortSignal已经不仅适用于 Fetch,还被广泛扩展至 DOM 事件监听(addEventListener的signal参数)、Node.js 流传输及定时器中,具有高度的一致性与组合能力。 - 超时使用 AbortSignal.timeout():在不需要人工点击取消、仅需网络超时的场景下,尽量使用原生的
AbortSignal.timeout(ms),能够大幅削减控制代码量。 - 维护遗留系统时明确 XHR 归零机制:若项目因兼容老旧浏览器而依赖
XMLHttpRequest,务必在xhr.abort()调用后妥善清理事件监听,防范闭包内存泄漏,且每次请求都应保证实例的独立性。
JavaScript怎么遍历对象的Symbol键和不可枚举属性
2026-11-05 13:13:39
封装一个健壮的JS字符串判空工具函数
2026-11-05 12:16:14
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
2026-11-05 11:22:29
JavaScript立即执行函数(IIFE)是什么及基本语法
2026-11-05 10:51:19
JS对象数组转字符串为什么推荐用JSON.stringify()
2026-11-05 09:40:31








