题干与适用场景
搜索页会并发请求建议、画像和结果接口。用户输入新关键词或离开页面时,旧请求应尽快取消;网络长时间无响应时则应超时,但页面从后台恢复后不能把冻结时间误算成请求耗时。请用 AbortSignal.timeout()、AbortSignal.any() 和 AbortController 设计取消策略。
这道题适合前端、Web 性能和异步架构岗位。重点是区分用户取消、超时、页面生命周期和真实网络失败,避免把所有异常都显示成“请求超时”。
面试官考察点
强回答会指出 AbortSignal.timeout() 返回自动中止的信号,超时原因是 TimeoutError;用户或控制器主动中止通常是 AbortError。还应说明超时基于活动时间,页面进入 bfcache 或 Worker 暂停时计时会暂停;多个信号可通过 AbortSignal.any() 合并,并保持可诊断的原因。
回答前需要澄清的问题
- 哪些请求可以安全取消,写入操作是否需要完成或查询状态?
- 超时是从用户操作、请求发出还是页面可见时开始计算?
- 用户取消、组件卸载、路由离开和超时是否需要不同提示?
- 目标浏览器是否支持静态方法,降级是否要保留手动计时器?
- 请求重试、去重、缓存和结果竞态如何处理?
30 秒回答框架
“我会为每个请求合并用户取消信号和 AbortSignal.timeout(),把 TimeoutError、AbortError 与网络错误分开处理。超时采用活动时间语义,所以页面挂起或 bfcache 不应被误报为在线耗时。新查询先取消旧控制器,响应还要用请求序列或信号检查防止旧结果覆盖新结果。对不支持静态方法的浏览器保留控制器加计时器的降级,并测试取消、超时、恢复和竞态。”
分步骤深入解答
第一步:区分三种取消来源
AbortController.abort() 是应用主动取消;AbortSignal.timeout(ms) 在活动时间达到阈值后自动取消;AbortSignal.any([...]) 在任一输入信号取消时触发。它们都能传给 fetch,但业务应保留取消原因,避免把用户离开页面当作服务故障。
const controller = new AbortController();
const timeout = AbortSignal.timeout(5000);
const signal = AbortSignal.any([controller.signal, timeout]);
fetch(url, { signal });第二步:按错误原因分类
超时会以 TimeoutError DOMException 作为原因,用户按下取消或浏览器停止操作通常是 AbortError。DNS、连接重置和 CORS 等问题可能表现为其他错误。捕获逻辑应先检查 signal.reason 或异常名称,再决定提示、重试和日志级别。
第三步:理解活动时间
timeout() 的计时基于活动时间,不是简单的墙上时钟。文档指出页面处于 bfcache 或 Worker 被挂起时,计时会暂停。它适合“用户可感知的活动请求窗口”,不等同于服务端绝对截止时间;服务端仍需自己的超时和幂等策略。
第四步:处理搜索竞态
用户连续输入时,先取消旧控制器,再为新查询创建信号。即使旧请求已经完成网络处理,响应回调仍可能排队;提交状态前要检查请求序列、当前关键词或 signal.aborted。取消请求不能替代结果版本校验。
第五步:区分可取消读写
搜索、图片和建议读取通常可以取消;支付、订单或文件上传等写操作可能已经在服务端生效。取消客户端等待不等于撤销服务端副作用。对写请求应使用幂等键、状态查询或后台任务,而不是简单加一个更短的 timeout。
第六步:设计组合信号的生命周期
组件卸载时调用控制器,路由切换时复用页面级信号,单请求再叠加 timeout。不要把一个已取消的全局信号永久共享给后续请求;每次操作都创建新的可用信号。需要诊断时,为不同来源设置明确的 reason。
第七步:提供兼容降级
旧浏览器若不支持 AbortSignal.timeout 或 any,可用 AbortController 和 setTimeout 实现同等取消,但要清理计时器并统一错误原因。能力检测应在运行时完成,默认路径仍要正确处理无 AbortSignal 的环境。
第八步:验证页面生命周期和资源清理
测试快速输入、组件卸载、路由离开、后台挂起、bfcache 返回、超时、用户取消、网络失败和重复重试。确认网络请求被取消、计时器清理、旧结果不覆盖新结果、错误提示准确,且没有因捕获异常而产生未处理 Promise 或状态更新警告。
设计取舍与边界
组合信号减少了取消胶水代码,但不会让服务端事务可撤销,也不会保证响应绝不抵达。活动时间语义更贴近用户体验,却不适合衡量端到端 SLA。超时值应按请求类型、网络环境和重试预算设定,并与服务端截止时间协调。
不要用 Promise.race 单独模拟超时后就遗留底层请求;如果不传 AbortSignal,网络和资源仍可能继续消耗。也不要把所有中止都记录为错误,否则用户正常导航会污染告警。
落地计划与证据
先为搜索读取建立请求状态机:创建信号、发起请求、按原因分类、提交带版本结果、卸载时取消。记录请求类型、原因、活动耗时、重试次数和最终状态,不记录敏感查询词。
在支持和不支持静态方法的浏览器中比较行为,覆盖 bfcache、Worker、低网速和快速输入。验收要求无旧结果覆盖、无定时器泄漏、取消提示准确、写请求不被误取消,并保留服务端幂等或状态查询证据。
常见误区与追问
把 TimeoutError 和 AbortError 混为一谈
用户取消、组件卸载和超时的下一步不同。按 reason 分类,分别决定提示、日志和重试。
以为客户端取消撤销了服务端写入
网络请求可能已到达服务端。写操作需要幂等键、查询状态或补偿流程。
用 Promise.race 代替真正取消
竞速只改变等待结果,不会自动停止底层 fetch。把 AbortSignal 传给请求并清理资源。
忽略 bfcache 的活动时间
页面挂起期间 timeout 可能暂停。不要把恢复后的耗时直接当成在线请求 SLA。
如何避免旧搜索结果覆盖新结果?
取消旧控制器并不充分;提交前还要比较查询序列、版本或当前关键词,确保响应顺序不改变状态。