[轉] 異步任務取消機制
異步任務取消機制
作者:謝杰
該文章是并發異步操作系列文章第一篇。
為什么需要取消異步任務
在現代的 Web 和 Node.js 應用中,我們經常需要啟動一些耗時較長的異步任務,比如:
- 下載大文件
- 進行高強度的計算
- 持續監聽一個長時間的事件流
然而,一旦任務開始運行,傳統上我們只能等待它結束或因異常而中斷。在很多情況下,這并不理想,因為我們可能需要主動終止任務,例如:
- 用戶已經切換到其他頁面,不再關心結果
- 請求超時
- 用戶點擊了“取消”操作
在早期的 Web API 中,XHR 提供過 .abort() 方法,但基于 Promise 的 fetch() 等 API 最初并沒有類似的能力。
為了填補這一空白,WHATWG 在 2017 年將 AbortController 和 AbortSignal 納入 DOM Standard,作為統一的取消信號機制。
和早期的 XHR 的實現所不同的是,早期的 XHR 里的 .abort() 方法,這是 XHR 自己實現的取消邏輯,其他 API 用不了。AbortController 則是 WHATWG 定義的統一標準,任何 API 只要愿意支持,都可以接收它的 signal 來實現取消(例如 fetch、ReadableStream、Node.js 的 fs.readFile、setTimeout 等等)。
雖然它最初出現在瀏覽器端,但這套設計與 DOM 并非深度耦合,只依賴于事件派發機制,因此 Node.js 自 v15.0.0(2020 年)起原生支持了 AbortController 與 AbortSignal(低于 v15 的版本可通過 node-abort-controller 等 polyfill 實現兼容)。如今,你可以在 Node 環境下的 fetch()、stream、timers 等異步 API 中,使用與瀏覽器一致的取消方案。
快速上手
接下來我們來看一下核心 API,有兩個:
- AbortController
- AbortSignal
為啥會有兩個呢?來看下面的圖:
上面的示意圖展示了 AbortController 與 AbortSignal 的工作原理。
在沒有取消機制時(上圖),客戶端發起異步請求后,只能一直等待服務器完成耗時處理,即便中途不再需要結果,也無能為力。
引入取消機制后(下圖),AbortController 就像一只“遙控器”,可以在任務執行過程中隨時發出 AbortSignal。異步任務收到信號后,會立即停止執行,從而避免無意義的等待和資源消耗。
你可以將這套設計理解為:
- Controller:信號的發射端
- Signal:信號的接收端
這種模式讓異步任務的生命周期可控,在請求超時、用戶主動取消或頁面切換等場景下,都能優雅、安全地終止任務。
下面來看一下代碼層面具體該怎么寫:
// 創建一個 AbortController 實例,用于管理和發出取消信號
const controller = new AbortController();
// 從控制器中獲取對應的 AbortSignal 對象
// 這個 signal 會被傳遞給需要支持取消的 API
const signal = controller.signal;
// 使用 fetch 發送請求,并在配置中傳入 signal
// 一旦 signal 被觸發(abort),fetch 會立即中止
fetch("https://example.com", { signal })
.then(res => {
// 當請求正常完成時會進入這里
console.log("請求成功");
})
.catch(err => {
// 如果是因為調用了 controller.abort() 而中斷,會返回 AbortError 類型的錯誤
if (err.name === "AbortError") {
console.log("請求被中斷");
} else {
// 其他類型的錯誤,例如網絡錯誤或服務器返回非 2xx 狀態碼
console.error("請求發生錯誤", err);
}
});
// 模擬延遲 2 秒后手動中止請求
// 當執行 controller.abort() 時,會觸發 signal 的中止狀態
// 已經關聯了這個 signal 的 fetch 請求會被立即終止
setTimeout(() => controller.abort(), 2000);
在這個示例中,我們用最簡單的方式演示了 AbortController 和 AbortSignal 如何配合 fetch() 實現“可中途取消”的網絡請求。
首先,我們創建了一個 AbortController 實例,它就像任務的“遙控器”,專門用來發出取消的指令。接著,通過 controller.signal 獲取與之綁定的 AbortSignal 對象。這個 signal 會被傳遞給支持取消機制的 API(例如 fetch),作為任務的監聽端。
當 fetch 啟動后,它會一直監聽這個 signal 的狀態。如果我們在任務執行中調用了 controller.abort(),signal 會立即變為“中止”狀態,fetch 也會立刻終止請求,并拋出一個 AbortError。
在這段代碼里,我們用 setTimeout 模擬了一個“2 秒后取消請求”的場景——這在實際開發中可能對應著請求超時、用戶點擊取消按鈕、或頁面切換等情況。
這種模式的好處是,你可以用一套統一的方式去管理異步任務的生命周期,不需要依賴特定 API 提供的私有取消方法,從而讓取消邏輯更優雅、更易維護。
相關細節
在了解了 AbortController 的核心理念后,接下來我們來看一下該 API 一些細枝末節的知識。
前面我們聊了 AbortController 的用途和基本用法,但如果你真的要在項目里用得順手,還是得知道它有哪些“開關”和“按鈕”。這一節,我們就來扒一扒它的細節——屬性、方法、甚至是那些看似不起眼的靜態方法。
1. controller.signal
這是 AbortController 唯一的屬性,也是它存在的核心意義。
你可以把它理解為 “傳話的麥克風”——只要控制器這邊說“停”,所有拿著這個 signal 的任務都會聽到。
- 類型:
AbortSignal - 常用場景:傳給
fetch()、ReadableStream、setTimeout等支持取消的 API。
const controller = new AbortController();
const { signal } = controller;
fetch("/api/data", { signal })
.then(r => r.json())
.then(console.log)
.catch(err => {
if (err.name === "AbortError") console.log("被取消了");
else console.error(err);
});
// 3 秒后取消
setTimeout(() => controller.abort("timeout"), 3000);
2. controller.abort([reason])
這是 AbortController 最常用的方法,也是你按下“終止任務”按鈕的地方。
-
作用:觸發
signal的abort事件,讓關聯的異步任務立刻中止。 -
可選參數
reason:可以傳一個自定義原因,比如"Timeout"或一個Error對象,這樣在捕獲時就能知道為什么被中止。注意點:調用
controller.abort()一次就夠了,多次調用沒有額外效果(signal.aborted會保持true)。
const c = new AbortController();
const s = c.signal;
fetch("/slow", { signal: s }).catch(err => {
// 在現代瀏覽器/Node 里,err.name === "AbortError"
// 同時 err.message / s.reason 里能拿到“取消原因”
if (s.aborted) {
console.log("已取消,原因:", s.reason); // e.g. "user-cancel" or Error(...)
}
});
document.getElementById("cancel").addEventListener("click", () => {
c.abort("user-cancel");
});
3. 靜態方法 AbortSignal.abort(reason)
聽到 AbortSignal.abort() 這個名字,你可能會疑惑:它和實例方法 controller.abort() 到底有什么不同?
區別在于:AbortSignal.abort() 是一個快捷工廠——直接生成一個已經處于中止狀態的 AbortSignal,免去了你先 new AbortController() 再手動調用 abort() 的步驟。
它非常適合那種一開始就確定要中止的場景,比如你在寫一個工具函數時,發現條件不滿足,就立即返回一個“無效”的信號,讓后續邏輯立刻停下。
來看個例子:假設我們有一個支持取消的文件下載工具函數:
async function downloadFile(url, signal) {
// 檢查這個 AbortSignal 是否已經處于中止狀態
// 如果已中止,立即拋出 AbortError,避免發起無意義的請求
// 這是 AbortSignal 提供的同步檢查方法(現代瀏覽器/Node.js 均支持)
signal.throwIfAborted();
const res = await fetch(url, { signal });
return res.blob();
}
現在有個需求:
- 用戶必須登錄才能下載文件
- 如果未登錄,不僅要阻止下載,還要避免發起任何網絡請求
用 AbortSignal.abort() 就能優雅地實現:
function getDownloadSignal(isLoggedIn) {
if (!isLoggedIn) {
// 創建一個“已中止”的 signal,原因是未登錄
return AbortSignal.abort(new Error("User not logged in"));
}
const controller = new AbortController();
return controller.signal;
}
// 模擬未登錄
const signal = getDownloadSignal(false);
// 這里會立刻拋出 AbortError
downloadFile("/big-file.zip", signal)
.then(() => console.log("下載完成"))
.catch(err => {
if (err.name === "AbortError") {
console.log("下載被取消:", signal.reason);
} else {
console.error(err);
}
});
這樣做的好處:
- 零浪費:不會發送無意義的請求。
- 調用方邏輯一致:依舊通過捕獲
AbortError處理取消,無需特殊分支。 - 寫法簡潔:少了創建控制器再手動
abort()的步驟。
4. 靜態方法 AbortController.timeout(ms)
這是 Node.js 從 v15.4.0 開始加的“便捷版”(瀏覽器端目前大部分不支持,需自行 polyfill)。
- 作用:創建一個
AbortSignal,并在指定的毫秒數后自動進入中止狀態。 - 優勢:不用自己寫
setTimeout再調用abort(),更簡潔。 - 典型場景:請求超時控制,比如
fetch(url, { signal: AbortController.timeout(5000) })。
// 5 秒超時:到點自動進入 aborted 狀態
const signal = AbortSignal.timeout(5000);
fetch("/maybe-slow", { signal })
.then(r => r.text())
.then(console.log)
.catch(err => {
if (err.name === "AbortError") {
console.log("超時了");
} else {
console.error(err);
}
});
總的來講,AbortController 的設計非常簡潔。
唯一的屬性是 signal,唯一的實例方法是 abort()。
但配合 AbortSignal,它能在異步任務中提供一致、優雅的取消能力。再加上靜態方法的加持,你既能自己掌控中止時機,也能用“一鍵定時取消”快速處理超時場景。
進階思考
設想這樣一個場景:你在網頁上點擊了“刪除數據庫記錄”,請求很快發了出去,服務器那邊的 SQL 語句已經開始準備執行。突然,你反悔了,立刻調用 controller.abort()。此時,這個刪除動作真的還能被阻止嗎?
現實可能會讓你有些失望——大多數情況下,答案是否定的。
AbortController 的取消邏輯,其實只作用在客戶端,它能做到的包括:
- 讓瀏覽器立即停止發送或接收數據
- 讓綁定了該信號的異步任務(如
fetch())立刻結束,并拋出AbortError - 丟棄本地的任務結果,不再進行后續處理
但是,一旦請求已經抵達服務器并開始執行,客戶端的中止信號并不能“遠程撤銷”后端的操作。換句話說,刪除數據庫的 SQL 語句可能已經在運行,你的 abort() 并不能讓它立刻停下來。
這就像你在餐館點了一份大餐,訂單已經送到廚房,廚師正忙著下鍋。這時你接到一個急事電話,必須馬上離開——雖然你對服務員說“不用了”,但廚房那邊其實已經在做,這道菜并不會因為你離開而自動停下。
如果真想實現全鏈路取消,就必須有后端的配合——在任務執行過程中檢查取消信號,并在檢測到時主動中止操作。
因此,AbortController 在前端任務管理中非常實用,但要想讓它變成“全局任務終止按鈕”,前后端需要協同設計取消機制。
寫在最后
在現代前后端開發中,異步任務的可控性越來越重要。無論是瀏覽器端的 fetch() 請求、WebSocket 長連接,還是 Node.js 中的文件流、定時器任務,能夠在合適的時機中止它們,意味著更好的資源管理和更流暢的用戶體驗。
AbortController 與 AbortSignal 為我們提供了一套統一且優雅的取消機制——它不依賴具體 API 的私有實現,而是通過“控制器 → 信號”的模式,讓不同類型的異步任務都能用相同的方式進行管理。這不僅降低了心智負擔,也讓取消邏輯更容易抽象成可復用的工具。
當然,我們也要認識到它的邊界:取消信號只在客戶端生效,并不能直接干預已經在服務器上執行的邏輯。如果需要真正的“全鏈路取消”,就必須在后端配合檢查取消狀態,并在合適的時機主動中止處理。
因此,在實際項目中,你可以把 AbortController 看作是異步任務的本地總開關:
- 它能幫你優雅地處理請求超時、用戶主動取消、頁面卸載等場景;
- 它能統一異步任務的生命周期管理方式;
- 它能減少資源浪費,避免無意義的等待和處理。
熟練掌握并恰當使用 AbortController,會讓你的異步代碼不僅能“跑起來”,更能“收得住”。
-EOF-

浙公網安備 33010602011771號