如何優雅地取消 JavaScript 非同步任務
在程式中處理非同步任務通常比較麻煩,尤其是那些不支援取消非同步任務的程式語言。所幸的是,JavaScript 提供了一種非常方便的機制來取消非同步任務。
中斷訊號
自從 ES2015 引入了 Promise,開發者有了取消非同步任務的需求,隨後推出的一些 Web API 也開始支援非同步方案,比如 Fetch API。TC39 委員會(就是制定 ECMAScript 標準的組織)最初嘗試定義一套通用的解決方案,以便後續作為 ECMAScript 標準。但是後來討論不出什麼結果來,這個問題也就擱置了。鑑於此,WHATWG (HTML 標準制定組織)另起爐灶,自己搞出一套解決方案,直接在 DOM 標準上引入了AbortController
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屬性是核心所在。該屬性是AbortSignalDOM 介面的例項,它有一個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 取消非同步任務內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!