題幹與適用場景
搜尋框在每次輸入變化後請求建議清單。使用者快速輸入 hello,瀏覽器依序發出 h、he、hel、hell、hello 五個請求,但回傳順序不受輸入順序約束。舊請求可能最後完成,把 hello 的結果改寫成 hell;使用者也可能清空輸入、切換網路或離開頁面。
請設計請求生命週期、結果提交規則、取消策略、快取與過期策略、載入和錯誤狀態、無障礙回饋以及驗證方案。回答要說明「介面顯示的結果對應哪個查詢」,不能只說加入防抖。
本題適合高階前端、React 與 UI 基礎設施面試。React 官方文件直接用快速輸入說明資料取得的 race condition,並建議在 Effect 清理時忽略過期回應;MDN 說明 AbortController.abort() 可以終止 fetch、回應本體讀取或串流。web.dev 的 stale-while-revalidate 資料補充以舊資料換取即時回饋的快取取捨。這些來源支援主題的技術代表性,但不能證明任何公司的固定原題或具體頻率。本題歸入 frontend,因為核心能力是瀏覽器非同步狀態、渲染一致性與互動回饋。
面試官考察點
第一,看候選人能否區分請求完成和請求仍有資格提交結果。Promise 先完成不代表它對應目前查詢;每次查詢需要穩定身分,提交前要檢查身分或查詢鍵仍然相符。
第二,看是否知道取消只是資源管理。AbortController 可以減少無用工作,但取消可能發生在伺服器已經處理之後,不能把 abort 當作業務回滾,也不能取代過期結果保護。
第三,看能否設計完整狀態機:空輸入、首次載入、已有結果刷新、成功、空結果、可重試錯誤、過期快取和元件卸載都應有明確規則。載入新查詢時保留舊結果還是顯示骨架屏,必須結合查詢語義和誤導風險決定。
最後,看驗證是否能控制請求回傳順序,並涵蓋快速輸入、清空、重試、快取命中、卸載和無障礙播報,而不是只測試網路按順序回傳的成功路徑。
回答前需要釐清的問題
- 每次輸入都必須請求嗎? 如果只在停止輸入後請求,可以使用防抖;但防抖只能減少請求數量,不能解決已發出的請求亂序。
- 舊結果可以繼續顯示嗎? 搜尋建議通常可以在刷新期間保留舊清單,但必須標記它對應的舊查詢;金融報價或權限結果可能應清空以避免誤導。
- 快取按什麼鍵失效? 查詢字串、篩選器、語言、帳戶權限和資料版本都可能屬於鍵。只按頁面 URL 快取會混入不同權限或篩選條件的資料。
- 伺服器是否支援取消或請求去重? 客戶端仍需要正確性保護;伺服器支援取消只改善資源回收,不能保證舊請求沒有副作用。
- 輸入是否需要即時讀屏回饋? 結果數量或載入狀態可以透過已有的
status區域禮貌播報,但不能每個字元都打斷使用者。
30 秒回答框架
「我會把每次查詢表示為帶序號的請求鍵,只有仍等於目前查詢鍵的回應才允許更新結果。元件清理時呼叫 AbortController.abort() 釋放網路和讀取資源,但不把取消當作回滾;即使取消失敗,過期回應也會被身分檢查丟棄。輸入停止後再發請求只是降噪,不能代替競態保護。狀態區分空值、載入、舊結果刷新、成功、空結果和可重試錯誤;快取按完整查詢鍵儲存,並設定明確的新鮮度。測試會強制讓舊請求晚於新請求回傳,涵蓋清空、卸載、重試、快取和讀屏回饋。」
分步深入解答
1. 先定義結果提交不變量
維護 currentKey、status、visibleData 和可選快取。currentKey 至少包含正規化查詢字串以及會改變結果的篩選器、語言和帳戶範圍。每次發出請求生成唯一 requestId,閉包保存自己的 key 和 controller。
結果提交前檢查兩個條件:請求沒有被標記為過期,而且它的 key 仍等於目前 key。只有通過檢查,才寫入可見結果、錯誤或成功狀態。這個不變量比「最後發出的請求通常最後回傳」可靠,因為網路不提供這種順序保證。
可以把顯示狀態看作純投影:目前查詢鍵、最新可用快取、目前仍有效請求和錯誤資訊共同決定介面。不要用多個 Effect 互相同步 loading、data 和 error,否則清空輸入或快速切換時容易把舊錯誤寫回新查詢。
2. 防抖、節流與競態保護分工
防抖把連續輸入合併成一次請求,適合搜尋建議;節流限制固定時間窗內的請求,適合捲動或即時監控。兩者都只影響「何時發請求」,不能阻止已發出的舊請求晚到。
即使使用 250 毫秒防抖,使用者仍可能在一次請求飛行期間繼續輸入。請求身分檢查必須保留。React 官方範例使用 Effect cleanup 設定 ignore,讓舊回應不再呼叫 setResults;同一原則也可以用遞增序號、請求鍵比較或狀態機實作。
3. 取消是最佳化,不是正確性證明
每個活動請求擁有自己的 AbortController。查詢鍵改變、輸入清空、元件卸載或開始替代請求時,可以呼叫 abort()。捕獲例外時將 AbortError 視為預期的取消結果,不把它顯示成失敗,也不覆蓋新查詢的狀態。
取消訊號可能來不及阻止伺服器處理,也可能只終止瀏覽器讀取。於是「取消舊請求」與「舊請求沒有產生業務副作用」是兩件事。搜尋 GET 通常沒有寫入副作用,但仍必須保留 requestId 檢查;如果請求包含不可逆動作,取消語義更需要伺服器冪等契約。
4. 選擇舊結果、骨架屏和快取
首次查詢沒有資料時顯示載入佔位和可存取的狀態文字。已有結果刷新時可繼續顯示舊結果,同時標記它對應的查詢鍵並顯示輕量刷新指示;如果舊結果會讓使用者誤選,則清空或降低其可操作性。
快取鍵應包含完整查詢輸入。快取命中可以立即展示,再在背景驗證;但要記錄時間、來源和錯誤,避免把過期或權限變化的資料當成最新事實。stale-while-revalidate 的核心是先用可接受的舊值回應,再非同步取得新值,產品必須先定義可接受的新鮮度視窗。
5. 處理錯誤、空結果和重試
空結果是成功狀態,不應顯示成網路錯誤。錯誤狀態要綁定目前 key;舊查詢的錯誤到達時只能被丟棄。可重試錯誤保留查詢輸入和退避資訊,使用者重試時生成新的 requestId,不重用已被判定過期的 Promise。
如果回傳 401、權限範圍變化或伺服器提示查詢條件無效,應清理或重新驗證快取,而不是無限重試。網路恢復後可重新請求目前 key,但要防止恢復事件把使用者已經清空的查詢重新填回介面。
6. 保持鍵盤與輔助技術回饋
輸入焦點不應因結果刷新而移動。結果容器使用穩定的清單 key,避免舊結果卸載導致螢幕閱讀器重複朗讀。載入、結果數量和錯誤可以放在預先存在的禮貌 status 區域,而且只在狀態發生有意義變化時更新。
鍵盤使用者應能在載入期間繼續輸入、取消或選擇結果;如果結果過期,選擇動作要再次確認它對應的查詢鍵。顏色不能是唯一的載入或錯誤線索,錯誤文字要與輸入或清單建立明確關聯。
7. 用可控順序測試狀態機
測試傳輸層應能暫停每個請求並手動釋放回應。至少涵蓋:舊請求晚於新請求成功回傳;舊請求失敗晚於新請求成功;使用者清空後舊回應到達;快取命中後背景刷新失敗;元件卸載後回應到達;取消拋出 AbortError;重試期間再次輸入。
每個事件後斷言目前 key、可見結果、狀態、快取時間和播報文字。還要驗證伺服器回傳相同結果但順序不同時不會重複渲染或遺失焦點。效能指標可以記錄請求數、取消率、過期回應丟棄數、可見結果延遲和錯誤重試率,但不能用更少請求掩蓋錯誤結果。
高品質示範回答
「我先把完整查詢正規化成 key,例如文字、篩選器、語言和權限範圍。每次實際請求生成 requestId 和 AbortController,並記錄目前 key。回應提交前必須同時通過 requestId 未過期和 key 仍相符兩個檢查;失敗、空結果和成功都遵守同一規則。輸入變化時取消舊 controller,捕獲 AbortError 後靜默結束,但我不會認為 abort 已經撤銷伺服器處理。
我會在停止輸入 250 毫秒後發請求降低雜訊。首次載入顯示佔位;已有結果刷新時,如果舊結果仍安全,會保留並標記為上一查詢的結果,否則清空。快取按完整 key 保存,允許先顯示短時間內的舊值,再在背景驗證並更新;快取超過業務允許的新鮮度就只顯示載入。
狀態機區分空輸入、載入、舊結果刷新、成功、空結果和可重試錯誤。錯誤必須綁定 key,舊請求晚到只能被丟棄。焦點保持在輸入框,結果使用穩定身分,狀態訊息透過預先存在的禮貌區域播報,不逐字播報。
最後我會用可控網路強制 hell 晚於 hello 回傳、清空後舊回應到達、取消失敗、快取刷新失敗、卸載後回應到達和重試時再次輸入,並斷言最終介面只解釋目前 key。」
常見錯誤
- 只做防抖 → 已發出的請求仍可能亂序 → 保留 requestId 或 key 檢查。
- 只顯示最後完成的回應 → 網路完成順序不等於查詢新舊順序 → 只允許目前 key 的回應提交。
- 把 abort 當作回滾 → 伺服器可能已處理請求 → 取消用於資源回收,正確性靠身分和版本規則。
- 所有查詢共用一個快取項 → 篩選器、語言或權限可能串資料 → 快取鍵覆蓋全部結果輸入。
- 舊錯誤覆蓋新查詢 → 使用者看到已失效的報錯 → 錯誤狀態綁定 requestId 和 key。
- 首次載入和刷新都顯示空白 → 使用者失去可用上下文 → 按誤導風險決定保留舊結果或顯示骨架。
- 每次輸入都播報狀態 → 螢幕閱讀器被連續打斷 → 只播報有意義的載入、結果和錯誤變化。
- 只測試順序成功 → 真實競態沒有涵蓋 → 控制回應、取消、清空、卸載和重試順序。
追問及應對
如果結果需要分頁或無限捲動,key 還夠嗎?
需要把篩選器、排序、游標或頁碼納入請求鍵,並把每頁資料與查詢版本綁定。新查詢應丟棄舊分頁;同一查詢載入下一頁時,上一頁可以繼續顯示,但重複游標和過期頁不能覆蓋已確認清單。伺服器若回傳 next cursor,應以伺服器游標為準,不能用客戶端頁碼推斷順序。
使用者在離線狀態輸入,重新連線時如何處理?
搜尋通常不需要持久化每個舊請求;保留目前輸入和最後一次可接受快取即可。連線後只請求目前 key,取消或忽略離線期間的過期請求。若產品要求離線建議,快取必須記錄時間、資料範圍和新鮮度提示,不能把離線結果偽裝成即時結果。
可以只用 React Query 或 SWR 解決嗎?
快取庫能提供去重、快取、失效和生命週期管理,但產品仍要定義查詢鍵、結果是否可陳舊、錯誤重試和互動狀態。庫的取消或 stale-time 預設值不能取代對權限、誤導風險和無障礙回饋的判斷。應驗證庫的請求競態語義,再把規則寫進 query key 和 UI 狀態。
如果搜尋請求改成 POST 並記錄稽核,取消還安全嗎?
不能假設安全。POST 可能有伺服器副作用,客戶端 abort 不表示交易回滾。應讓伺服器提供冪等鍵、明確提交狀態和查詢介面;前端只在取得權威確認後顯示成功。若只是複雜查詢且沒有副作用,仍可使用 POST,但要把語義和重試契約寫清楚。