題幹與適用場景
這道程式設計題考察並行調度,而不是把所有 Promise 放進 Promise.all。候選人需要在吞吐、順序、錯誤傳播、取消和空輸入之間定義清晰契約,並證明不會超出並行上限或重複消費項目。
面試官考察什麼
- 能否用固定 worker 或等價調度器保持最多 N 個活動任務。
- 能否按輸入索引寫回結果,不讓完成順序改變輸出順序。
- 能否明確 fail-fast 與收集錯誤的語意,並在拒絕後處理仍在執行的任務。
- 能否正確處理 N 非法、空輸入、同步拋錯、取消和 mapper 回傳非 Promise 值。
回答前需要澄清的問題
先確認是否允許任務在取消後繼續執行、fail-fast 時是否需要等待清理、是否要回傳部分結果,以及 mapper 是否接受 AbortSignal。也要確認是否限制重試、是否允許動態調整並行、錯誤物件是否必須保留原始引用。
30 秒回答框架
我會建立不超過 N 個 worker,共享下一個索引。每個 worker 取一個索引,使用 Promise.resolve 包住同步回傳和同步拋錯,再按索引寫入結果。fail-fast 只傳播第一項錯誤並停止派發新任務;collect 模式記錄每項狀態。AbortSignal 觸發後停止取新索引,並把訊號傳給 mapper。時間複雜度 O(items),額外空間是結果與狀態 O(items),峰值活動任務 O(N)。
分步驟深入解答
1. 先定義結果與錯誤契約
結果必須與輸入同序。collect 模式回傳 fulfilled/rejected 狀態和原始錯誤,不能用 undefined 混淆成功值。fail-fast 回傳第一個觀察到的錯誤,但要說明已開始的任務不會被 JavaScript 自動殺死;若 mapper 支援訊號,可以主動請求取消。
2. 固定 worker 和索引分配
把下一個索引放在共享計數器中,每個 worker 迴圈取值,直到越界、取消或 fail-fast 標記出現。任務啟動前遞增索引,確保一個項目只被一個 worker 領取。worker 數量取 min(N, items.length),避免閒置 worker 和無意義的 Promise。
3. 處理同步拋錯與完成順序
呼叫 mapper 時要捕獲同步拋錯,再用 Promise.resolve 統一非同步完成。成功或失敗都按原索引寫入狀態;不能在完成回呼中直接 push 到陣列。一個 worker 完成後立刻領取下一個索引,以維持吞吐但不增加活動數量。
4. 傳播取消並停止派發
如果 signal 已取消,先拒絕或回傳約定的取消錯誤,不再啟動新任務。執行中任務收到同一個 signal;調度器等待 worker 退出後再 settle,避免呼叫方看到主 Promise 已結束而背景仍無限派發。取消和業務錯誤要有可區分的錯誤原因。
5. 驗證邊界與複雜度
測試空陣列、N 為 0、N 大於輸入、mapper 同步回傳、同步拋錯、不同延遲、第一項失敗、多個失敗和中途取消。用計數器斷言活動任務從未超過 N,用日誌或測試鉤子確認每個索引只呼叫一次。總 mapper 呼叫 O(items),結果空間 O(items),調度器額外並行 O(N)。
高品質示範回答
我會校驗 N 為正整數,建立 min(N, items.length) 個 worker,共享下一個索引。worker 取得索引後呼叫 mapper,並用 Promise.resolve 處理同步值和同步拋錯;結果按索引寫回,所以完成順序不會改變輸出。fail-fast 設定停止派發標誌並傳播首個錯誤,collect 模式保留每項狀態。signal 取消後不再領取新索引,並把 signal 傳給執行中的 mapper,等待 worker 退出。測試延遲亂序、同步錯誤、空輸入、N 邊界和取消,斷言並行峰值不超過 N。
常見錯誤
- 直接
Promise.all(items.map(mapper)),一次性啟動全部任務。 - 用
push收集結果,導致輸出按完成順序而不是輸入順序。 - 只捕獲 Promise rejection,漏掉 mapper 的同步拋錯。
- fail-fast 後繼續派發新任務,或誤以為 reject 會自動取消已執行任務。
- 把取消當成普通業務錯誤,導致呼叫方無法區分重試與使用者主動停止。
- 沒有驗證 N、空輸入和 mapper 回傳普通值的邊界。
追問及應對
為什麼不用遞迴啟動下一項?
遞迴可以表達串行鏈,但必須額外維護並行計數、錯誤和取消;固定 worker 更容易證明活動任務上限,也避免深遞迴和重複派發。
fail-fast 時已經開始的任務怎麼辦?
JavaScript Promise 沒有通用強制終止。停止派發新任務,並在 mapper 支援 AbortSignal 時呼叫 abort;否則等待它們自然結束,同時避免再寫入已對外結束的結果。
collect 模式應該怎樣表達錯誤?
用每項狀態物件區分 fulfilled 和 rejected,並保留原始 value 或 reason。不要用特殊佔位值,因為成功值本身可能是 undefined 或 null。
如何動態調整並行?
把最大 worker 數和目前活動數分開管理,在安全邊界增加或停止領取索引。動態調整會增加證明和測試成本,除非需求明確,否則先採用固定上限。