异步任务取消不是“锦上添花”,而是高并发、重交互应用下的默认生存能力。本文带你从真实场景出发,系统梳理 7 种主流取消方案 + 最佳实践,让你一次搞懂「何时取消、怎么取消、取消到什么程度」。
一、为什么需要取消异步任务
| 原因 | 说明 |
|---|---|
| 资源优化 | 避免无用网络请求 / 计算浪费 |
| 状态一致性 | 过期结果不再写入当前状态 |
| 用户体验 | 用户改主意、跳转页面时及时止损 |
| 错误预防 | 组件已卸载,回调却仍在执行 → 内存泄漏 / 白屏 |
二、高频真实场景
- 组件卸载后仍请求 → 写已销毁的 state
- 搜索框连续输入 → 前面联想请求已无意义
- 用户狂点按钮 → 只应保留最后一次
- 路由跳转 → 旧页面请求全部废弃
- 大文件上传超时 → 超过 30 s 自动中断
三、7 种取消实现方式(含代码)
| 方案 | 适用场景 | 关键 API / 思路 |
|---|---|---|
| 标志位 | 极简 demo、一次性逻辑 | isCancelled = true |
| AbortController | Fetch、主流浏览器 | fetch(url, {signal}) |
| clearTimeout / clearInterval | 定时器 | clearTimeout(id) |
| 可取消 Promise 包装 | 任意 Promise | reject({isCancelled:true}) |
| RxJS unsubscribe | 流式场景 | subscription.unsubscribe() |
| React useEffect 清理 | 组件生命周期 | return () => controller.abort() |
| 并行任务统一取消 | 多请求竞速 | Promise.all + 自定义 cancelToken |
下面给出可直接粘贴运行的完整示例
1. 标志位取消法(原理级演示)
let isCancelled = false;
function asyncTask() {
return new Promise(resolve => {
setTimeout(() => {
if (!isCancelled) resolve('任务完成');
else console.log('任务已取消');
}, 1000);
});
}
asyncTask().then(console.log);
setTimeout(() => (isCancelled = true), 500);
缺点:异步逻辑仍在跑,仅结果不生效。
2. AbortController 取消 Fetch(浏览器原生)
const controller = new AbortController();
const { signal } = controller;
fetch('https://api.example.com/data', { signal })
.then(r => r.json())
.then(console.log)
.catch(err => {
if (err.name === 'AbortError') console.log('请求已取消');
else console.error('请求出错', err);
});
setTimeout(() => controller.abort(), 500);
优点:浏览器自动终止 TCP 连接,真正省流量。
3. 定时器取消
const timer = setTimeout(() => console.log('不会执行'), 1000);
clearTimeout(timer); // 同理 clearInterval
4. 可取消 Promise 包装器(库级别通用)
function makeCancellable(promise) {
let isCancelled = false;
const wrapped = new Promise((resolve, reject) => {
promise.then(
val => (isCancelled ? reject({ isCancelled, val }) : resolve(val)),
err => (isCancelled ? reject({ isCancelled, err }) : reject(err))
);
});
return { promise: wrapped, cancel: () => (isCancelled = true) };
}
// 使用
const task = makeCancellable(new Promise(r => setTimeout(() => r('ok'), 1000)));
task.promise.then(console.log).catch(e => e.isCancelled && console.log('取消'));
setTimeout(() => task.cancel(), 500);
5. RxJS 流取消
import { interval } from 'rxjs';
const sub = interval(1000).subscribe(console.log);
setTimeout(() => sub.unsubscribe(), 5000);
6. React 组件内取消(useEffect)
import { useEffect, useState } from 'react';
function DataFetcher({ id }) {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
(async () => {
try {
const res = await fetch(`/api/data/${id}`, { signal: controller.signal });
setData(await res.json());
} catch (e) {
if (e.name !== 'AbortError') console.error(e);
}
})();
return () => controller.abort(); // 组件卸载或 id 变化时取消
}, [id]);
return <div>{data ? JSON.stringify(data) : '加载中...'}</div>;
}
7. 并行任务统一取消(Promise.all 版)
function cancelablePromise(promise, cancelToken) {
return new Promise((resolve, reject) => {
cancelToken.promise.then(() => reject(new Error('取消')));
promise.then(resolve, reject);
});
}
const cancelTokenSource = {
promise: new Promise(res => (cancelTokenSource.cancel = res))
};
Promise.all([
cancelablePromise(fetch('/api/data1'), cancelTokenSource),
cancelablePromise(fetch('/api/data2'), cancelTokenSource)
])
.then(console.log)
.catch(console.warn);
setTimeout(() => cancelTokenSource.cancel(), 1000);
四、优雅最佳实践(生产级 checklist)
- 优先原生:Fetch / Axios 新版已支持
signal,直接用 - 封装复用:提供统一
useCancellableRequest或AsyncCanceller类 - 错误区分:取消 ≠ 异常;
AbortError单独处理,不弹红屏 - 生命周期:组件卸载、路由跳转、页面隐藏时统一取消
- 超时兜底:
Promise.race([fetch, timeout])防止永久挂起 - 用户反馈:取消后给出 Loading → 已取消 的明确提示
- 可观测:打点记录「取消率」,大促期间可省 30%+ 带宽
五、总结
异步取消不是边缘功能,而是性能、稳定性、体验的三重保险:
- 选对场景 → 用对 API → 封装统一 → 监控可观测
记住这四步,你的应用就能在流量洪峰下优雅止损,避免“取消忘记打、内存泄漏、数据串台”的经典三连坑。
把上面的 7 段代码全部跑通,你对 JS 异步取消的掌握就超过 90% 开发者了。祝你编码愉快!