題幹與適用場景
一個任務編輯器會立即顯示使用者提交的標題。使用者可以在任何請求完成前先提交標題 A,再提交標題 B。第二個回應可能先回來,任一請求都可能失敗,兩個請求等待期間還可能完成一次背景重新擷取,其他使用者也可能更新同一任務。每次寫入被接受後,伺服器端會回傳權威任務資料和單調遞增的資源版本。
請設計客戶端狀態、請求契約、順序策略、失敗復原、衝突體驗、無障礙回饋和驗證方案。要求不只是讓介面看起來更快。無論請求按什麼順序結束,介面狀態都必須能由「伺服器端權威狀態 + 仍待確認的使用者意圖」解釋出來。
本題適合資深前端、UI 基礎設施和前端系統設計面試。公開前端面試資料明確討論樂觀更新、請求競態、錯誤狀態和回復;一份 2025 年公開面經也記錄了面試官繼續追問樂觀更新的原理與適用場景。這些來源能證明該主題在目前環境中具有代表性,但不能證明它是某家公司的固定原題,也不能證明具體面試頻率。本題歸入 frontend,因為核心能力是瀏覽器端非同步狀態與互動設計;伺服器端並行控制只是設計輸入,不是主要實作目標。
面試官考察點
第一,看候選人能否拆開權威狀態和推測狀態。直接替換快取中的物件,並儲存一個舊快照用於回復,只適用於單一隔離請求。如果後續樂觀操作依賴同一物件,這種做法就會出錯。穩健的模型保留最新已確認基礎狀態和有序的待確認意圖,再從兩者推導介面。
第二,看候選人能否識別操作語意。「只接收最新回應」可以保護一條客戶端算繪路徑,卻無法阻止伺服器端最後處理更早送出的請求。候選人必須決定操作應序列化、合併、改寫成可交換形式,還是交給伺服器端強制排序。把標題設為 B、加一、切換狀態、刪除和付款命令的選擇不同。
第三,看能否只復原受影響的操作。如果提交 B 後 A 才失敗,回復 A 之前的整份快照可能把 B 一併抹掉。高品質回答會刪除或標記失敗的 A,只用有效的權威回應推進基礎狀態,再重播剩餘意圖。如果伺服器端版本衝突導致重播不再安全,介面應顯示目前伺服器端值,讓使用者明確解決衝突。
最後,看正式環境處理:待確認和錯誤狀態對鍵盤與輔助技術使用者仍然可操作;不會把取消請求誤認為伺服器端回復;測試會強制涵蓋所有回應順序、失敗順序、重新擷取競態、重試和衝突,而不是只驗證正常路徑。
回答前需要釐清的問題
- 一次操作的語意是什麼? 絕對賦值
setTitle("B")可以覆蓋更早的標題草稿,increment(1)可能要求兩次操作都提交;toggle()在重試時含義不清,傳送明確目標值更安全。操作語意決定能否合併。 - 儲存等待期間還能否再次提交? 停用控制項能得到簡單的序列化流程,但可能破壞編輯體驗。如果必須允許繼續輸入,應把本機草稿分開儲存,再排隊或合併提交,或者使用帶版本的平行協定。
- 誰決定寫入順序? 如果伺服器端只有無條件的「最後到達者獲勝」,客戶端必須序列化傳送順序相關的儲存。若 API 接受基礎版本或客戶端序號,並拒絕過時寫入,才可以考慮受控平行。
- 樂觀操作是否低風險且可逆? 按讚、標籤和草稿往往適合樂觀回饋。付款、破壞性操作、受權限控制的修改和產生不可逆外部效果的操作,可能應該顯示待確認,而不是提前宣稱成功。
- 背景資料來源是否會更新同一筆紀錄? 重新擷取、訂閱、另一個瀏覽器分頁和協作者都可能改變基礎狀態。狀態模型要識別資源版本,並定義待確認意圖能否安全重播到新基礎上。
- 使用者需要感知什麼? 明確每一列是否需要儲存中、重試和衝突標記,焦點能否移動,以及哪些儲存或失敗訊息需要播報,同時避免每次按鍵都觸發即時區域訊息。
30 秒回答框架
「我會把最新伺服器端確認的任務作為基礎狀態,把每次本機提交表示為帶 ID 的意圖,介面把待確認意圖疊加在基礎狀態上。對標題替換,我預設每個任務只有一個進行中的儲存,並把排隊草稿合併成最新標題;請求 ID 只能防止舊回應覆蓋介面,不能阻止伺服器端最後寫入舊請求。成功時我採用回傳的權威版本、刪除對應意圖並重播剩餘意圖;失敗時只刪除失敗意圖,不回復整份舊快照。版本衝突會暫停自動重播並顯示雙方值。我會提供逐項待確認和錯誤狀態、保持焦點,並測試亂序回應、成功失敗交錯、重新擷取競態、重試、衝突和離線復原。」
分步驟深入解答
1. 在選擇函式庫之前先定義一個不變量
每個資源保留一個已確認的 base 和一個有序的 pending 清單。每個待確認項包含穩定的客戶端操作 ID、意圖與承載資料、提交順序和目前狀態。顯示狀態是一個純投影:
view = fold(base, pending in logical order, applyIntent)不變量是:介面等於最新被伺服器端接受的狀態,再疊加所有仍有資格執行的本機意圖。這樣,晚到的重新擷取或回應只是對帳輸入,不會直接獲得覆蓋介面的權力。
React 的樂觀 reducer 模式體現了相同的分離:Action 等待期間基礎值發生變化時,React 可以基於新基礎重新執行 reducer。客戶端快取函式庫可以管理 mutation 生命週期回呼,但不會替產品決定操作語意。無論實作使用 React state、TanStack Query、其他快取還是自訂 store,這個不變量都適用。
不要維護 serverTask、formTask、optimisticTask 三份互相獨立的資料,再用零散 Effect 同步。尚未提交的表單草稿要分開儲存,因為輸入還不是 mutation;提交時再把它轉換成擁有穩定身分的意圖。
2. 根據操作語意選擇順序,而不是根據延遲選擇
常用策略有三種:
| 策略 | 適用場景 | 代價或風險 |
|---|---|---|
| 依資源序列化 | 順序相關的寫入;API 沒有順序保護 | 後續操作需要等待,但伺服器端順序可證明 |
| 序列化並合併 | 只有最後一個未傳送值重要,例如連續提交標題 | 中間已提交值會被有意捨棄 |
| 帶伺服器端契約的平行 | 操作獨立或可交換,或 API 強制基礎版本/客戶端序號 | 吞吐更高,但必須明確處理對帳和衝突 |
對於這個標題編輯器,依任務 ID 序列化,並合併尚未傳送的標題變化。如果 A 進行中時又提交 B,介面立即顯示 B,但網路佇列只保留 B 作為下一次寫入。A 結束後,再用最新已接受版本傳送 B。TanStack Query 文件說明 mutation 預設平行,並提供 mutation scope 讓同一 scope 序列化;不使用該函式庫也可以實作相同策略。
只有語意更強時才安全平行。API 可以拒絕過時的基礎版本、接受同一編輯工作階段單調遞增的客戶端序號,或者提供真正可交換的操作。React 官方文件也明確提醒,自訂非同步 Transition 不保證請求順序,仍需使用提供順序保證的高階 Action 或明確佇列。只在瀏覽器記錄最新請求 ID,能夠防止舊回應把可見的 B 改回 A,卻無法阻止伺服器端最終存成 A。取消 A 也不能證明伺服器端沒有提交 A。
3. 成功對帳不能相信到達順序
每個回應都應標明對應操作,並回傳權威資源和版本。序列化模式下,處理目前活動操作,採用回傳的基礎狀態,刪除該操作,再重播排隊意圖。A 完成時,B 仍處於待確認狀態,所以介面不會閃回 A。
受控平行時,先用操作 ID 比對回應,再驗證資源版本或確認資訊。不能因為 Promise 已完成就直接指定回應資料。早於目前確認版本的回應不能替換基礎狀態;只能刪除該回應明確確認的操作。如果伺服器端會正規化資料,例如裁剪標題,應先把正規化結果作為新基礎,再重播後續本機意圖。
背景重新擷取遵循相同規則。目前基礎是版本 11 時,回傳版本 12 的擷取結果可以推進基礎狀態;若待確認意圖語意允許,再把它們疊加到版本 12。回傳版本 10 的結果是過時證據,不能讓基礎狀態倒退。
4. 依操作復原,不能依快照復原
假設 A 和 B 都已經樂觀顯示,隨後 A 失敗。回復 A 之前擷取的物件,會把 B 一起刪除。正確做法是標記或刪除 A,保留 B,再重新計算投影。絕對標題賦值可以繼續基於目前基礎傳送 B;順序相關的增量操作則可能要等待、重新計算,或因原前提不再成立而失敗。
把失敗分成可行動的狀態:
- 驗證或權限失敗在使用者修改輸入或權限前是終態;在控制項附近顯示被拒絕值和伺服器端原因。
- 短暫網路失敗可以提供重試。只有伺服器端契約保證重試安全時才重用同一操作身分;否則要先確認原請求是否已經提交。
- 版本衝突表示基礎狀態被其他人改變。採用或重新取得目前伺服器端值,比較本機意圖;只有合併規則得到證明時才自動重播。標題衝突應顯示目前值和提議值,不能無聲二選一。
- 未知結果表示請求可能已經提交,只是回應遺失。「本機回復後按新操作重試」可能重複不可冪等的行為。
有些操作不應樂觀顯示。如果失敗難以撤銷、會改變授權、會扣款,或會製造誤導性的法律或業務狀態,應立即顯示真實的待確認回饋,等伺服器端接受後再確認成功。
5. 讓推測狀態可見且無障礙
樂觀狀態不應偽裝成已確認狀態。標記受影響任務正在儲存,保留已提交值,並提供局部重試或衝突入口。不要停用無關任務。採用序列化時,還要區分目前進行中的儲存與較新的排隊值,讓監控和錯誤訊息能夠對應正確意圖。
儲存成功、失敗或快取對帳時保持鍵盤焦點。狀態區域可以播報「正在儲存任務標題」「任務標題已儲存」或「儲存失敗」,且不需要移動焦點。W3C 的 status 角色具有禮貌播報的即時區域語意;狀態容器應在訊息變化前就存在,也不要每次按鍵都播報。欄位錯誤要與對應欄位建立關聯,視覺狀態也不能只依賴顏色。
6. 測試狀態機,也要觀察策略結果
測試所選順序策略,不能假裝每種策略都允許相同事件軌跡。序列化路徑要先斷言 B 已經可見但在 A 結束前不會送出,再涵蓋 A 成功或失敗後 B 成功或失敗、回應遺失後的對帳,以及伺服器端正規化。對任何受支援的平行路徑,使用可控傳輸層和伺服器端 stub,分別強制 A 後 B、B 後 A 的處理順序與回應順序。每個事件後都斷言介面投影、排隊操作、確認版本和最終伺服器端值。
繼續注入 mutation 等待期間的新版本擷取和舊版本擷取、協作者衝突、離線後復原、元件卸載再掛載、重複提交,以及伺服器端提交後才收到取消。驗證鍵盤焦點、狀態播報、重試標籤,以及錯誤是否關聯正確的任務和操作。
正式環境指標要把感知速度與正確性分開:樂觀算繪延遲、確認延遲、失敗與回復率、衝突率、佇列等待、被合併操作數、重試次數和對帳不一致。一個很快但經常突然改成意外值的介面,沒有滿足產品契約。
高品質示範回答
「我會先確認每個提交標題是否都必須儲存,還是只保留使用者最後一次意圖。這個場景只需要最終標題,API 也會回傳資源版本,因此同一任務不需要平行寫入。我會允許使用者繼續編輯,但依任務 ID 序列化網路儲存。如果 A 進行中時使用者又提交 B,介面立即顯示 B,B 成為合併後的排隊意圖。
每個任務的狀態包含確認基礎、一個活動操作和最多一個排隊標題意圖。每個操作都有客戶端 ID 和提交時使用的基礎版本。顯示標題優先取排隊值,其次是活動樂觀值,最後才是確認標題。A 成功後,我採用回應中的權威任務和版本。因為 B 還在等待,介面不會顯示 A;隨後用新版本傳送 B。A 失敗時,我只刪除 A;如果是短暫故障,仍允許儲存 B。驗證或權限失敗會一直關聯被拒絕的意圖。
如果伺服器端回報其他使用者已經推進版本,我會暫停自動提交,取得或採用目前標題,並顯示伺服器端值和提議值供使用者解決。我不會只忽略舊回應,因為這不能阻止舊請求最後寫入伺服器端;也不會把 abort 當作回復。
每個任務都有自己的儲存中、已排隊、失敗或衝突狀態。焦點保留在編輯器內,預先存在的禮貌狀態區域只播報提交結果,不播報每個字元。測試會分別控制 Promise 回傳和伺服器端處理順序,以證明亂序成功、成功失敗交錯、過時擷取、衝突、回應遺失、重試、離線復原和元件卸載後的最終介面與伺服器端值。」
常見錯誤
- 儲存一份 mutation 前快照,任何錯誤都回復它 → 晚到的失敗會抹掉更新的樂觀操作 → 刪除失敗操作,再用確認基礎和剩餘意圖重新計算。
- 只保留最新回應 ID → 介面可能正確,伺服器端卻最後提交了舊請求 → 序列化順序相關的寫入,或要求伺服器端強制版本/序號語意。
- 使用一個全域 loading 標記 → 無關控制項被凍結,錯誤也無法關聯資源 → 依資源和操作 ID 追蹤待確認與錯誤狀態。
- 假設所有 mutation 都能重播 → 切換、增量、刪除和不可逆命令的失敗語意不同 → 啟用樂觀行為前定義明確意圖和合併規則。
- 認為取消就撤銷了請求 → 伺服器端可能在收到取消前已經提交 → 對帳權威狀態,並為未知結果設計重試。
- 允許任何擷取結果覆蓋快取 → 過時擷取或遲到回應讓確認狀態倒退 → 比較資源版本,並把有效待確認意圖重播到最新基礎上。
- 隱藏全部待確認狀態來營造即時感 → 使用者無法解釋後續修正,也無法重試對應操作 → 保留樂觀值,同時顯示局部儲存、失敗和衝突狀態。
- 只依提交順序測試成功路徑 → 最難的競態沒有被執行 → 在測試矩陣中控制回應順序、伺服器端順序、失敗、重新擷取、重試和重新掛載。
追問及應對
如果使用者可以離線編輯幾個小時,需要改變什麼?
待確認操作必須持久化,且綁定已登入帳戶和資源;重播前要重新整理授權與權威版本。儲存意圖語意和穩定操作 ID,不能儲存擷取的介面快照。重新連線後先取得基礎狀態,捨棄使用者明確取消的操作,只重播擁有有效合併規則的操作。權限過期、資源已刪除和資料結構變化都需要可見的阻塞狀態。長期離線協作可能超出簡單樂觀佇列能力,需要領域專用合併操作或協同編輯協定。
伺服器端回傳版本後,是否所有 mutation 都能平行?
不能。版本能說明回應代表哪個狀態,卻不會自動定義兩次寫入的順序或合併方式。只有伺服器端原子檢查提交的基礎版本、強制序號,或操作互相獨立、真正可交換時,平行才安全。否則兩個絕對賦值仍可能按錯誤順序到達並提交。對單一順序相關資源,序列化往往是更清楚的契約。
伺服器端分配真實 ID 時,如何樂觀建立紀錄?
算繪前產生穩定客戶端 ID,同時用作操作身分和臨時清單 key。成功後記錄客戶端 ID 到伺服器端 ID 的對應,替換確認實體,不要讓無關列重新掛載。若伺服器端支援,用操作身分為重試去重。建立失敗時只刪除或標記該樂觀實體。後續若要操作臨時實體,應等待 ID 對應,或使用確認後能夠重寫目標的操作佇列。
什麼時候應該主動選擇悲觀介面?
如果提前顯示成功會嚴重誤導使用者、復原不能在本機完成、衝突常見,或者操作不可逆且風險高,就應選擇悲觀介面。付款、權限授予、法律提交或破壞性批次操作可以立即確認使用者已點擊並顯示真實待處理狀態,等權威確認後再顯示成功。介面仍可以即時回應,但不能聲稱尚未發生的結果。