如何取消一個已經(jīng)開始的 JavaScript Promise
在現(xiàn)代前端開發(fā)中,Promise 和 async/await 已經(jīng)成為處理異步操作的基石。然而,一個常見的棘手問題是:如何取消一個已經(jīng)開始的 Promise?
比如,用戶發(fā)起一個數(shù)據(jù)請求,但在請求完成前又導(dǎo)航到了其他頁面;或者用戶在一個搜索框中快速輸入,我們需要取消前一次的搜索請求,只保留最后一次。在這些場景下,取消一個進行中的 Promise 就顯得至關(guān)重要。
核心問題:為什么 Promise 本身不可取消?
首先,我們需要理解 Promise 的核心設(shè)計理念。一個 Promise 代表一個異步操作的最終結(jié)果。它的狀態(tài)一旦從 pending(進行中)變?yōu)?nbsp;fulfilled(已成功)或 rejected(已失敗),就永遠不會再改變。
Promise 本身不提供取消機制,原因如下:
- 狀態(tài)不可逆:這是 Promise 的核心規(guī)范。一旦狀態(tài)改變,就形成了一個確定的、不可變的結(jié)果。
- 單一責(zé)任:Promise 的職責(zé)是傳遞價值和狀態(tài),而不是控制異步操作本身的執(zhí)行流程。發(fā)起異步操作的函數(shù)(如 fetch)才是執(zhí)行者。
打個比方:你寄出了一封信(發(fā)起了一個 Promise),你不能在信件投遞過程中把它神奇地從郵政系統(tǒng)里撤回來。你能做的,是在信件送達時(Promise 完成時),選擇忽略它。
我們的目標(biāo),就是實現(xiàn)這種“忽略”機制,并盡可能地通知底層的異步操作停止工作,以節(jié)省資源。
AbortController
AbortController 是目前實現(xiàn) Promise 取消的最佳實踐和標(biāo)準(zhǔn)方案。它最初是為取消 fetch 請求而設(shè)計的,但其通用性使其可以與任何異步操作集成。
AbortController 的工作方式:
- 創(chuàng)建一個 AbortController 實例。
- controller.signal:這是一個 AbortSignal 對象,可以傳遞給需要支持取消的異步函數(shù)(如 fetch)。
- controller.abort():調(diào)用此方法來發(fā)出“中止”信號。
- 當(dāng) abort() 被調(diào)用時,signal 會通知所有監(jiān)聽它的異步操作。對于 fetch 來說,它會自動中止網(wǎng)絡(luò)請求并讓 Promise reject 一個名為 AbortError 的錯誤。
(1) 與 fetch 配合使用
這是 AbortController 最常見的用法。
在 async/await 語法中同樣清晰:
(2) 在自定義 Promise 中使用 AbortController
你也可以讓你自己的異步函數(shù)支持 AbortSignal。
原理:
- 你的函數(shù)需要接收 signal 作為參數(shù)。
- 在異步操作的關(guān)鍵節(jié)點,檢查 signal.aborted 屬性。如果為 true,則提前退出。
- 使用 signal.addEventListener('abort', ...) 來注冊清理邏輯(如清除定時器)。
優(yōu)點:
- 官方標(biāo)準(zhǔn):是 W3C 和 WHATWG 定義的標(biāo)準(zhǔn) API。
- 真正中止底層操作:fetch 會中止網(wǎng)絡(luò)連接,自定義函數(shù)也可以通過它來清理資源(如清除定時器),避免了不必要的浪費。
- 語義清晰:通過專門的 AbortError 來區(qū)分“取消”和“其他錯誤”,代碼更健壯。
- 組合性強:一個 AbortSignal 可以傳遞給多個 Promise,實現(xiàn)批量取消。
雖然 Promise 本身的核心設(shè)計使其不可變,但通過 AbortController 這一強大的模式,我們已經(jīng)可以非常有效地控制和終止異步流程,編寫出更健壯、更高效的應(yīng)用程序。