在程序中處理異步任務通常比較麻煩,尤其是那些不支持取消異步任務的編程語言。所幸的是,JavaScript 提供了一種非常方便的機制來取消異步任務。
中斷信號
自從 ES2015 引入了 Promise ,開發者有了取消異步任務的需求,隨后推出的一些 Web API 也開始支持異步方案,比如 Fetch API。TC39 委員會(就是制定 ECMAScript 標準的組織)最初嘗試定義一套通用的解決方案,以便后續作為 ECMAScript 標準。但是后來討論不出什么結果來,這個問題也就擱置了。鑒于此,WHATWG (HTML 標準制定組織)另起爐灶,自己搞出一套解決方案,直接在 DOM 標準上引入了 AbortController。這種做法的壞處顯而易見,因為它不是語言層面的 ECMAScript 標準,因此 Node.js 平臺也就不支持 AbortController 。
在 DOM 規范里, AbortController 設計得非常通用,因此事實上你可以用在任何異步 API 中。目前只得到 Fetch API 的官方支持,但你完全可以用在自己的異步代碼里。
在開始介紹之前,我們先看下 AbortController 的工作原理:
const abortController = new AbortController(); // 1
const abortSignal = abortController.signal; // 2
fetch( 'http://kaysonli.com', {
signal: abortSignal // 3
} ).catch( ( { message } ) => { // 5
console.log( message );
} );
abortController.abort(); // 4
上面的代碼很簡單,首先創建了AbortController的一個實例(1),并將它的 signal 屬性賦值給一個變量(2)。然后調用fetch()并傳入 signal 參數(3)。取消請求時調用 abortController.abort()(4)。這樣就會自動執行fetch() 的 reject ,也就是進入catch()部分(5)。
它的signal屬性是核心所在。該屬性是 AbortSignal DOM 接口的實例,它有一個 aborted屬性,帶有是否調用了 abortController.abort()的相關信息。還可以在上面監聽abort事件,該事件在abortController.abort()調用時觸發。簡單來說,AbortController 就是AbortSignal的一個公開接口。
可取消的函數
假設有一個執行復雜計算的異步函數,為簡單起見,我們就用定時器模擬:
function calculate() {
return new Promise( ( resolve, reject ) => {
setTimeout( ()=> {
resolve( 1 );
}, 5000 );
} );
}
calculate().then( ( result ) => {
console.log( result );
} );
可能的情況是,用戶想取消這種耗時的任務。我們用一個按鈕來開始和停止:
<button id="calculate">Calculate</button>
<script type="module">
document.querySelector( '#calculate' ).addEventListener( 'click', async ( { target } ) => { // 1
target.innerText = 'Stop calculation';
const result = await calculate(); // 2
alert( result ); // 3
target.innerText = 'Calculate';
} );
function calculate() {
return new Promise( ( resolve, reject ) => {
setTimeout( ()=> {
resolve( 1 );
}, 5000 );
} );
}
</script>
上面的代碼給按鈕綁定了一個異步的 click 事件處理器(1),并在里面調用了 calculate() 函數(2)。5 秒后會彈出對話框顯示結果(3)。順便提一下,script[type=module]可以讓 JavaScript 代碼進入嚴格模式,跟 'use strict' 的效果一樣。
增加中斷異步任務的功能:
{ // 1
let abortController = null; // 2
document.querySelector( '#calculate' ).addEventListener( 'click', async ( { target } ) => {
if ( abortController ) {
abortController.abort(); // 5
abortController = null;
target.innerText = 'Calculate';
return;
}
abortController = new AbortController(); // 3
target.innerText = 'Stop calculation';
try {
const result = await calculate( abortController.signal ); // 4
alert( result );
} catch {
alert( 'WHY DID YOU DO THAT?!' ); // 9
} finally { // 10
abortController = null;
target.innerText = 'Calculate';
}
} );
function calculate( abortSignal ) {
return new Promise( ( resolve, reject ) => {
const timeout = setTimeout( ()=> {
resolve( 1 );
}, 5000 );
abortSignal.addEventListener( 'abort', () => { // 6
const error = new DOMException( 'Calculation aborted by the user', 'AbortError' );
clearTimeout( timeout ); // 7
reject( error ); // 8
} );
} );
}
}
代碼變長了很多,但是別慌,理解起來也不是很難。
最外層的代碼塊(1)相當于一個 IIFE(立即執行的函數表達式),這樣變量 abortController(2)就不會污染全局了。
首先把它的值設為null,并且它的值隨著按鈕點擊而改變。隨后給它賦值為AbortController的一個實例(3),再把實例的signal屬性直接傳給 calculate()函數(4)。
如果用戶在 5 秒之內再次點擊按鈕,就會執行abortController.abort()函數(5)。這樣就會在剛才傳給 calculate()的AbortSignal實例上觸發 abort 事件(6)。
在 abort 事件處理器里面清除定時器(7),然后用一個適當的異常對象拒絕 Promise(8)。
根據 DOM 規范,這個異常對象必須是一個'AbortError' 類型的DOMException。
這個異常對象最終傳給了catch (9) 和finally (10)。
但是還要考慮這樣一種情況:
const abortController = new AbortController(); abortController.abort(); calculate( abortController.signal );
這種情況下 abort 事件不會觸發,因為它在signal傳給calculate() 函數前就執行了。為此我們需要改造下代碼:
function calculate( abortSignal ) {
return new Promise( ( resolve, reject ) => {
const error = new DOMException( 'Calculation aborted by the user', 'AbortError' ); // 1
if ( abortSignal.aborted ) { // 2
return reject( error );
}
const timeout = setTimeout( ()=> {
resolve( 1 );
}, 5000 );
abortSignal.addEventListener( 'abort', () => {
clearTimeout( timeout );
reject( error );
} );
} );
}
異常對象的定義移到了頂部(1),這樣就可以在兩個地方重用了。另外,多了個條件判斷abortSignal.aborted(2)。如果它的值是true,calculate()函數應該立即拒絕 Promise,沒必要再往下執行了。
到這里我們就實現了一個完整的可取消的異步函數,以后碰到需要處理異步任務的地方就可以派上用場了。
到此這篇關于如何優雅地取消 JavaScript 異步任務的文章就介紹到這了,更多相關JavaScript 取消異步任務內容請搜索億速云以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持億速云!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。