问题场景
团队来了个需求:页面左侧有一个耗时 3-5 秒的实时日志流(SSE),用户切到右侧 Tab 时必须停止处理。你第一反应是:
// 切 Tab 时:强行丢弃数据,但流还在背后跑
onTabChange(() => {
ignoreLogs = true;
});
结果?网络没断、内存还在涨、事件还在触发。控制台一堆 setState 警告:"Can't perform a React state update on an unmounted component"。
这时候 AbortController 能救场——但大部分人只知道用它取消 fetch。
原因分析
AbortController 的核心是一个 信号机制:创建一个 signal 对象,传递给异步操作,什么时候你想停了,调用 controller.abort(),所有监听这个 signal 的操作同时收到终止通知。
但下面这三个用法,90% 的前端不知道。
解决方案
🥇 用法一:用 AbortSignal 取消事件监听
这是最惊艳的——事件监听器可以不用 removeEventListener。
const controller = new AbortController();
const { signal } = controller;
// 把 signal 作为第三个参数传进去
window.addEventListener('resize', handleResize, { signal });
document.addEventListener('click', handleClick, { signal });
// 👇 一键清除两个监听器
controller.abort();
实战场景: 弹窗/Modal 组件挂载时注册全局键盘事件(ESC 关闭),关闭时一键清理所有监听,无需逐个 removeEventListener 也不怕匿名函数。
useEffect(() => {
const ctrl = new AbortController();
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal();
}, { signal: ctrl.signal });
return () => ctrl.abort(); // 清理
}, []);
⚡ 注意:同一个 signal 只能用一次
abort(),之后 signal 状态就是aborted。
🥇 用法二:用 AbortSignal 中断 ReadableStream / Async Iterator
你的 SSE 实时日志读取用了 for await...of,怎么优雅中断?
async function readLogs(url, signal) {
const res = await fetch(url, { signal });
const reader = res.body.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (signal.aborted) {
await reader.cancel(); // 主动释放
break;
}
processLog(decoder.decode(value));
}
} catch (err) {
if (err.name !== 'AbortError') throw err;
}
}
或者更简洁——直接把 signal 传给 stream:
const res = await fetch(url, { signal });
const reader = res.body.getReader({ signal }); // 👈 signal 直接绑定
当 abort() 触发时,reader.read() 自动抛出 AbortError,完美中断。
🥇 用法三:用 AbortSignal 封装"超时取消"模式
AbortSignal.timeout() 是一个静态方法,创建一个 N 毫秒后自动 abort 的信号:
const res = await fetch(url, {
signal: AbortSignal.timeout(3000) // 3秒超时自动取消
});
这是原生 API,不需要任何 polyfill,比手写 setTimeout + controller 优雅十倍。
更绝的是组合信号:
const ctrl = new AbortController();
const timeoutSignal = AbortSignal.timeout(5000);
// 两个信号任意一个触发都算中止
fetch(url, {
signal: anySignal([ctrl.signal, timeoutSignal])
});
// 用户手动取消
ctrl.abort();
anySignal来自@rails/request.js或者自己实现。2025 年最新版 Chrome 已实验性支持AbortSignal.any()原生组合。
要点总结
| 用法 | 一句话 | 替代了什么 |
|---|---|---|
| 取消事件监听 | addEventListener 传 { signal } | removeEventListener + 函数引用 |
| 中断 Stream | 绑定 signal 到 reader | reader.cancel() 手动处理 |
| 超时 + 组合取消 | AbortSignal.timeout() + 组合信号 | setTimeout + 自定义状态变量 |
一句话总结: AbortController 是 JS 异步世界的"万能急停按钮"。不只在 fetch 里用,事件、流、定时器——任何支持 signal 的 API 都能被优雅中止。告别内存泄漏,从善用 AbortController 开始。