取消异步请求方案深度解析
一句话概括
AbortController 是浏览器原生的”取消按钮”——把它的 signal 塞进 fetch,随时调用 abort() 就能干净利落地中断请求,告别竞态 bug 和内存泄漏。
核心知识点
1. 三行代码看懂 AbortController
1
2
3
4
5
6
7
8
9
10
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.catch(err => {
if (err.name === 'AbortError') console.log('请求被取消了');
});
// 任何时候调用,请求就被掐断
controller.abort();
new AbortController()创建控制器,controller.signal是信号对象- 把
signal传给fetch(或 axios 的signal参数),API 内部就会监听它 - 调
controller.abort()→signal.aborted变true→ fetch 抛AbortError abort()还能传个reason:controller.abort('用户离开页面'),通过signal.reason拿到
2. React 组件卸载时自动取消
这是面试必问。关键只有一句话:在 useEffect 清理函数里调 abort()。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function UserCard({ id }) {
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${id}`, { signal: controller.signal })
.then(r => r.json())
.then(setUser)
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
// 👇 组件卸载或 id 变化时,自动取消上一次请求
return () => controller.abort();
}, [id]);
// ...
}
为什么能跑:useEffect 的清理函数会在依赖变化时先执行(取消旧请求),然后才跑新一轮 effect。
3. 搜索框防抖 + 取消,双保险
用户敲 “react” → 发请求A → 继续敲 “ hooks” → 立马取消A,发请求B。这是竞态处理的经典范式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function useSearch() {
const controllerRef = useRef(null);
const search = useCallback(async (keyword) => {
// 第一步:取消上一次请求
controllerRef.current?.abort();
// 第二步:创建本次请求的 controller
controllerRef.current = new AbortController();
try {
const res = await fetch(`/api/search?q=${keyword}`, {
signal: controllerRef.current.signal
});
return await res.json();
} catch (err) {
if (err.name === 'AbortError') return; // 被取消,正常情况,不理
throw err;
}
}, []);
// 组件卸载时清理
useEffect(() => () => controllerRef.current?.abort(), []);
return search;
}
配合防抖使用效果最佳——用户停止输入 300ms 后才发请求,期间上一个请求早已被 abort。
4. 同时管住一堆请求
一个 AbortController 只能取消共享同一个 signal 的请求。要分组管理多个场景(页面切走全取消、单个请求单独取消),可以自己封装:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class RequestPool {
#controllers = new Map();
fetch(key, url, opts = {}) {
this.#controllers.get(key)?.abort(); // 同 key 互斥
const ctrl = new AbortController();
this.#controllers.set(key, ctrl);
return fetch(url, { ...opts, signal: ctrl.signal });
}
abort(key) {
this.#controllers.get(key)?.abort();
this.#controllers.delete(key);
}
abortAll() {
this.#controllers.forEach(c => c.abort());
this.#controllers.clear();
}
}
const pool = new RequestPool();
pool.fetch('list', '/api/list');
pool.fetch('detail', '/api/detail/42');
// 页面切走
pool.abortAll();
5. AbortSignal.any() 组合多个取消源
有时候一个操作可能被多种原因取消:用户手动点取消 OR 组件卸载 OR 超时。用 AbortSignal.any() 把它们捆一起:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const userCancelCtrl = new AbortController();
const timeoutCtrl = new AbortController();
const unmountCtrl = new AbortController();
// 2 秒超时
setTimeout(() => timeoutCtrl.abort(), 2000);
// 任意一个信号触发,请求就被取消
const signal = AbortSignal.any([
userCancelCtrl.signal,
timeoutCtrl.signal,
unmountCtrl.signal,
]);
fetch('/api/heavy', { signal }).catch(err => {
if (err.name === 'AbortError') console.log('请求被中断:', signal.reason);
});
注意:AbortSignal.any() 是较新的 API(Chrome 116+),面试提到属于加分项。
其实你每天都在用
场景一:搜索框输入联想 你敲 “天气”,页面发了请求;继续敲 “天气预报”,上一个请求立刻被 abort。不是你网络快,是 AbortController 把旧请求掐了。没有它,你会先看到 “天气” 的结果闪现再被覆盖。
场景二:SPA 页面切换 从列表页点进详情页的瞬间,列表页还没返回的数据请求被取消。否则你到了详情页,列表请求才回来,setState 到已卸载的组件上——React 报黄牌警告 Can't perform a React state update on an unmounted component。
场景三:上传大文件中途后悔 你选了 2GB 的视频上传,进度条走到 30%,手滑选错了文件,点了取消。前端立马 controller.abort(),浏览器关掉 TCP 连接,不再浪费带宽——虽然服务器那 30% 已经写入磁盘了(这就是为什么非 GET 请求取消要在后端做幂等)。
场景四:表单提交防重复 用户狂点”提交”按钮,传统做法是 loading 状态 + disabled,但如果你用 AbortController,每次点击前先把上一次 abort 掉,确保同一时刻只有一个请求在路上。
常见误解
❌ 误区一:调了 abort(),服务器就不会收到请求了
HTTP 请求一旦从网卡发出,服务器大概率已经收到并开始处理。abort() 只是客户端不再等待响应,浏览器会关闭 TCP 连接。对于 GET 请求,反正数据不会变,重发就行。对于 POST/PUT,服务器可能已经写入数据了——所以非幂等接口建议带上幂等键(idempotency key)。
❌ 误区二:Promise 可以被取消
JavaScript 里原生 Promise 一旦创建就无法取消。pending → fulfilled/rejected 是不可逆的。网上看到那些”可取消 Promise”的方案,本质上都是额外包一层状态标记(let cancelled = false),在 .then 里判断——并不是真正取消了异步任务本身。真正的取消还是得靠 AbortController 把底层操作(fetch、定时器等)中断掉。
❌ 误区三:cancel 了就不用 catch 了
很多人以为 abort 之后万事大吉,结果控制台飘红。fetch 被取消时会抛 AbortError,它是一个 DOMException。如果你的代码里有 await fetch(...) 却没 try-catch 或者 catch 里不判断 error.name === 'AbortError',未捕获的 rejection 就出去了。
1
2
3
4
5
6
7
8
9
10
// ❌ 错误示范
await fetch(url, { signal }); // abort 时直接抛异常,没人接
// ✅ 正确写法
try {
await fetch(url, { signal });
} catch (err) {
if (err.name === 'AbortError') return; // 取消是预期行为,忽略
throw err; // 其他错误往上抛
}
❌ 误区四:axios 的 CancelToken 还能继续用
axios 从 v0.22 开始原生支持 signal 参数,CancelToken 已在 v1.0 被彻底移除。面试说 “axios 取消请求用 CancelToken” 等于告诉面试官你用的是三年前的文档。
1
2
3
4
5
6
7
8
9
// ❌ 过时的写法
const source = axios.CancelToken.source();
axios.get('/api', { cancelToken: source.token });
source.cancel();
// ✅ 现在的写法
const controller = new AbortController();
axios.get('/api', { signal: controller.signal });
controller.abort();
一句话总结
每个异步请求都配一个 AbortController,组件卸载就 abort——这就是现代前端处理请求取消的全部秘密,简单但面试官就想听你说出来。