先记住这个答案
AbortController 是浏览器原生的「取消器」。用法分三步:先 new AbortController(),再把 controller.signal 作为参数传给 fetch,要取消时调用 controller.abort()。fetch 收到信号后立刻中断请求,返回的 Promise 以名为 AbortError 的错误拒绝,所以 catch 里要把「主动取消」和「真的出错」区分开。
- controller 负责喊停,
signal负责传话 abort()后fetch以AbortError拒绝,要在catch里单独判断- 一个 controller 只能用一次,下次请求要 new 新的
- 超时取消直接用
AbortSignal.timeout(ms)
最常见的用法:搜索框取消旧请求
用户快速输入 a、ab、abc,会连发三个请求。如果 a 的请求最慢,它最后回来就会把 abc 的结果盖掉。解决办法是每次发新请求前,先把上一次的取消掉。
let controller = null
async function search(keyword) {
controller?.abort() // 取消上一次还没回来的请求
controller = new AbortController() // 一个 controller 只能用一次,每次都 new
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(keyword)}`, {
signal: controller.signal
})
render(await res.json())
} catch (err) {
if (err.name === 'AbortError') return // 主动取消的,不用提示用户
showError(err)
}
}这样界面上永远只显示最后一次输入的结果。打开 Network 面板能看到被取消的请求状态是 canceled。
注意 await res.json() 读响应体的过程也会被中断,所以它要放在同一个 try 里。
超时取消和批量取消
fetch 自己没有 timeout 参数,以前要自己写 setTimeout 加 abort。现在浏览器提供了现成的静态方法,一行就够。
try {
const res = await fetch('/api/report', {
signal: AbortSignal.timeout(5000) // 5 秒还没回来就自动取消
})
} catch (err) {
// 超时触发的错误名是 TimeoutError,不是 AbortError
if (err.name === 'TimeoutError') showError('请求超时,请重试')
}超时报的是 TimeoutError,用户主动取消报的是 AbortError,这样就能给出不同的提示。
- 一个
signal管多个请求 - 同一个
signal可以同时传给好几个fetch。比如一个页面同时拉了列表、详情、推荐三个接口,用户切走时调用一次abort(),三个请求一起取消。React 里通常放在useEffect的清理函数里做:组件卸载时调用controller.abort(),就不会出现「组件都没了,请求回来还在setState」的情况。 - 让自己写的异步函数也支持取消
signal不只fetch能用。自己封装的异步任务可以接收一个signal,监听它的abort事件来停止工作,addEventListener也支持传signal,取消时自动移除监听。这样整条异步链路都能用同一个 controller 统一取消。
容易答错的地方
- 以为取消了,服务端就不处理了
abort只是让浏览器断开连接、不再等响应。请求如果已经发到服务端,服务端大概率照常执行完。所以提交订单、扣款这类写操作,取消请求不等于操作没发生,防重复提交还是要靠按钮禁用、幂等 key 这些手段。- 复用同一个 controller
- controller 被
abort过一次,它的signal就永久处于已取消状态。拿它再发请求,fetch会立刻拒绝。每次新请求都要 new 一个新的 controller。 catch里不区分错误类型- 取消是正常业务行为,不该弹「网络错误」。
catch里先判断err.name === 'AbortError',是的话直接 return。
面试官还会怎么问?
axios 怎么取消请求?
axios 从 0.22 开始支持同样的写法,在配置里传 signal: controller.signal 即可,取消后 axios 抛出的错误可以用 axios.isCancel(err) 判断。以前的 CancelToken 写法已经废弃,新代码直接用 AbortController。
防抖和取消请求有什么区别,要一起用吗?
防抖是少发请求,停止输入 300 毫秒才发一次;取消是处理已经发出去的请求,防止旧结果覆盖新结果。防抖之后仍然可能有两个请求同时在路上,所以两个通常一起用。
不支持 signal 的老接口怎么防止旧结果覆盖?
给每次请求记一个递增的序号,结果回来时对比当前最新序号,不是最新的就丢弃。请求本身没取消,但旧数据不会再写进界面。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。