題幹與適用場景
搜尋頁會並發請求建議、畫像和結果介面。使用者輸入新關鍵字或離開頁面時,舊請求應盡快取消;網路長時間無回應時則應逾時,但頁面從背景恢復後不能把凍結時間誤算成請求耗時。請用 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。
如何避免舊搜尋結果覆蓋新結果?
取消舊控制器並不充分;提交前還要比較查詢序列、版本或目前關鍵字,確保回應順序不改變狀態。