先记住这个答案
AbortSignal.any() 接收一个 signal 数组,返回一个新的 signal。只要数组里任意一个 signal 被取消,新 signal 就跟着取消,它的 reason 就是最先取消的那个 signal 的 reason。典型用法是把用户手动取消的 controller.signal 和 AbortSignal.timeout(ms) 合在一起传给 fetch,这样一个请求同时拥有两个取消源,谁先触发都能中断它。
AbortSignal.any是「任意一个取消就取消」,不是全部- 合成
signal的reason取自最先取消的那个 - 数组里有已取消的
signal,结果一创建就是已取消 - 超时报
TimeoutError,手动取消报AbortError
用户取消加超时,合成一个 signal
比如一个导出报表的按钮,用户可以点「取消」,同时请求最多等 8 秒。以前要自己写 setTimeout 去调 abort(),现在直接合成。
const userController = new AbortController() // 绑在「取消」按钮上
cancelButton.onclick = () => userController.abort()
async function exportReport() {
const signal = AbortSignal.any([
userController.signal, // 用户点取消
AbortSignal.timeout(8000) // 8 秒超时
])
try {
const res = await fetch('/api/report/export', { signal })
return await res.blob()
} catch (err) {
if (err.name === 'TimeoutError') showToast('导出超时,请稍后重试')
else if (err.name === 'AbortError') showToast('已取消导出')
else throw err
}
}fetch 被取消时,拒绝的错误就是 signal 的 reason。AbortSignal.timeout 超时后 reason 是名为 TimeoutError 的错误,controller.abort() 不传参数时是 AbortError,所以靠 err.name 就能分清是谁取消的。
如果调用 abort() 时传了自己的原因,比如 abort(new Error('页面切走')),那 fetch 拒绝的就是这个 Error,判断时要按你自己定的规则来。
合成 signal 的几个行为细节
AbortSignal.any 的规则很少,但每一条面试都可能追问。
- 已经取消的
signal传进去会怎样 - 数组里只要有一个在传入时已经是取消状态,返回的
signal一创建就是aborted: true,它的reason就是那个signal的reason。把它传给fetch,请求根本不会发出,Promise 立刻拒绝。 - 取消只往一个方向传
- 来源取消会传给合成
signal,反过来不行。合成signal本身没有abort()方法,它不是 controller。想「一键取消所有」,就要额外建一个 controller 作为来源之一,调用它的abort()。 - 空数组
AbortSignal.any([])返回一个永远不会被取消的signal,因为没有任何来源能触发它。
老环境下怎么手写一个
AbortSignal.any 是比较新的 API,要兼容老浏览器时可以自己实现。思路是新建一个 controller,给每个来源注册一次 abort 事件监听,谁先触发就把它的 reason 转给自己。
anySignalJavaScriptfunction anySignal(signals) {
const controller = new AbortController()
for (const s of signals) {
if (s.aborted) { controller.abort(s.reason); break } // 已取消的直接生效
s.addEventListener('abort', () => controller.abort(s.reason), {
once: true,
signal: controller.signal // 合成 signal 取消后,自动移除其余监听
})
}
return controller.signal
}最后那个 signal: controller.signal 很关键。长期存在的来源,比如整个页面生命周期里的一个 controller,如果每次请求都往它身上挂监听却不移除,监听会越积越多。用 addEventListener 的 signal 选项,合成 signal 一旦取消,其余来源上的监听会被一并移除。
容易答错的地方
- 把
any理解成全部取消才取消 AbortSignal.any的语义和Promise.any不一样。Promise.any是等任意一个成功,AbortSignal.any是任意一个取消就取消,更接近Promise.race的「谁先来算谁」。- 以为取消了合成
signal能停掉来源 - 合成
signal是只读的。比如超时触发后,用户的userController并不会变成已取消,下次请求它还能继续用。 - 每次请求复用同一个合成
signal - 合成
signal一旦取消就永远是取消状态。超时来源AbortSignal.timeout也是从创建时开始计时的,所以要在每次发请求时现场调用AbortSignal.any,而不是在模块顶部建一个全局共用。
面试官还会怎么问?
怎么知道到底是哪个来源触发了取消?
看合成 signal 的 reason,它就是最先取消的来源的 reason。也可以逐个检查来源的 aborted 属性,但多个来源都可能已经取消,reason 更准确。给不同来源传不同的 reason,判断起来最清楚。
React 组件里怎么用?
在 useEffect 里建一个 controller,清理函数里调用 abort(),代表组件卸载。发请求时把它和 AbortSignal.timeout(ms) 合成一个,就同时有了卸载取消和超时取消。
一个封装好的请求函数要不要接收外部的 signal?
要。函数内部可以有自己的超时,同时接收调用方传进来的 signal,两者用 AbortSignal.any 合成。这样调用方想取消时不用了解内部细节,内部的超时也不会被外部覆盖掉。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。