有一类 bug 很隐蔽:用户在两个 tab 间快速切换,或者在搜索框里快速改关键词,最后界面显示的数据对不上当前的选择。
原因是请求竞态(race condition):先发的请求后回来,把后发请求的结果覆盖了。要根治它,需要能"取消不再需要的请求"。AbortController 就是 Web 平台为此提供的标准方案。
AbortController 是什么
AbortController 是一个用来"发取消信号"的对象。它配一个 signal,把 signal 交给支持中断的异步 API(最典型的是 fetch),之后调用 controller.abort() 就能中断它。
const controller = new AbortController()
fetch('/api/data', { signal: controller.signal })
.then((res) => res.json())
.then((data) => console.log(data))
.catch((err) => {
if (err.name === 'AbortError') {
console.log('请求被取消了')
} else {
console.error('请求出错', err)
}
})
// 在需要的时候取消
controller.abort()几个要点:
controller.signal是一个AbortSignal,传给fetch的signal选项。- 调用
controller.abort()后,正在进行的fetch会 reject,错误的name是'AbortError'。 - 一个 controller 用一次:abort 之后这个 signal 就永久处于 aborted 状态,再用它发请求会立刻被取消。要发新请求得 new 一个新的 controller。
场景一:搜索联想中的竞态
经典竞态场景:用户输入"a"、"ab"、"abc",发了三个请求。如果"ab"的响应比"abc"晚到,界面最后显示的就是"ab"的结果——和输入框里的"abc"对不上。
解决办法:发新请求前,先取消上一个。
let controller = null
async function search(keyword) {
// 取消上一次还没回来的请求
if (controller) controller.abort()
controller = new AbortController()
try {
const res = await fetch(`/api/search?q=${keyword}`, { signal: controller.signal })
const data = await res.json()
render(data) // 只有最新的请求能走到这里
} catch (err) {
if (err.name !== 'AbortError') throw err // 取消是预期行为,忽略;其它错误照常抛
}
}这样无论响应顺序如何,旧请求都被取消了,只有最新的请求结果会渲染。这比"用时间戳判断哪个是最新响应"更彻底——旧请求直接不占用网络和处理。
场景二:配合超时自动取消
fetch 本身没有超时机制。用 AbortController 加 setTimeout 就能实现"超过 N 秒自动取消":
function fetchWithTimeout(url, options = {}, timeout = 8000) {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeout)
return fetch(url, { ...options, signal: controller.signal })
.finally(() => clearTimeout(timer)) // 请求结束(成功/失败)都清掉定时器
}现代浏览器还提供了更简洁的 AbortSignal.timeout(),专门用于超时:
fetch('/api/data', { signal: AbortSignal.timeout(8000) })AbortSignal.timeout(8000) 返回一个 8 秒后自动 abort 的 signal,不用自己管定时器。
场景三:同时取消多个请求
一个 controller 的 signal 可以传给多个 fetch,一次 abort() 全部取消。适合"离开页面时取消该页所有在途请求":
const controller = new AbortController()
const { signal } = controller
Promise.all([
fetch('/api/user', { signal }),
fetch('/api/posts', { signal }),
fetch('/api/stats', { signal }),
])
// 一次性取消上面三个
controller.abort()反过来,AbortSignal.any([...]) 可以把多个 signal 合并,任意一个触发就取消(比如"超时"或"用户手动取消"任一发生):
const userCancel = new AbortController()
const signal = AbortSignal.any([userCancel.signal, AbortSignal.timeout(10000)])
fetch('/api/data', { signal }) // 超时 10s 或用户点取消,都会中断不只 fetch:监听 abort 事件
AbortSignal 不是 fetch 专属的。任何异步操作都能监听它的 abort 事件来响应取消,所以你可以用它给自己的异步逻辑加"可取消"能力:
function delay(ms, signal) {
return new Promise((resolve, reject) => {
const timer = setTimeout(resolve, ms)
signal?.addEventListener('abort', () => {
clearTimeout(timer)
reject(new DOMException('Aborted', 'AbortError'))
})
})
}addEventListener 也支持 signal 选项,用来自动移除监听器——这是个很优雅的清理技巧:
const controller = new AbortController()
window.addEventListener('resize', onResize, { signal: controller.signal })
window.addEventListener('scroll', onScroll, { signal: controller.signal })
// 一次 abort,上面两个监听器全部自动移除,不用逐个 removeEventListener
controller.abort()框架里的用法
Vue 3 组件卸载时取消在途请求,避免对已销毁组件 setState / 赋值:
<script setup>
import { ref, onUnmounted } from 'vue'
const data = ref(null)
const controller = new AbortController()
async function load() {
try {
const res = await fetch('/api/data', { signal: controller.signal })
data.value = await res.json()
} catch (err) {
if (err.name !== 'AbortError') console.error(err)
}
}
load()
onUnmounted(() => controller.abort()) // 组件销毁,取消请求
</script>React 里在 useEffect 的清理函数里 abort,是处理"组件卸载/依赖变化"竞态的标准写法:
useEffect(() => {
const controller = new AbortController()
fetch(`/api/user/${id}`, { signal: controller.signal })
.then((r) => r.json())
.then(setUser)
.catch((err) => {
if (err.name !== 'AbortError') console.error(err)
})
return () => controller.abort() // 清理:id 变化或卸载时取消上一个请求
}, [id])常见坑
第一,把 AbortError 当成真错误。 abort 是你主动触发的预期行为,catch 里要先判断 err.name === 'AbortError' 并忽略它,否则会弹出一堆"请求失败"的误报。
第二,复用已经 abort 的 controller。 controller 是一次性的,abort 后必须 new 一个新的。常见错误是把 new AbortController() 放在了循环外只创建一次。
第三,只取消了请求,没处理"取消后还要不要更新状态"。 取消请求只是不再等它的结果,但如果你的代码里 abort 后又走到了赋值逻辑,仍可能出问题。竞态场景下,确保被取消的分支不会再改 UI。
第四,以为 abort 能停止服务器处理。 abort 只是客户端不再接收响应,服务器可能已经处理完了(比如已经写了数据库)。它解决的是"前端别用过期响应",不是"撤销服务端操作"。
总结
AbortController 是 Web 平台取消异步操作的统一标准,核心用法就一套:
const controller = new AbortController()
fetch(url, { signal: controller.signal })
controller.abort()记住几个要点:
- 竞态:发新请求前 abort 旧请求(搜索联想、tab 切换)。
- 超时:
AbortSignal.timeout(ms)或自己配setTimeout。 - 批量:一个 signal 传给多个 fetch,一次取消;
AbortSignal.any合并多个信号。 - 清理:
addEventListener传signal选项,自动移除监听器。 - catch 里放过
AbortError,它是预期行为不是错误。
把它用顺手,那些"数据对不上"的竞态 bug 就能从根上消失。