中止信號(Abort signal)
在將 Promise 引入 ES2015 並出現了一些支持新異步解決方案的 Web API 之后不久,需要取消異步任務的需求就出現了。最初的嘗試集中在創建通用解決方案上,並期待以后可以成為 ECMAScript 標准的一部分。但是,討論很快陷入僵局,無法解決問題。因此,WHATWG 准備了自己的解決方案,並以 AbortController 的形式將其直接引入 DOM。這種解決方案的明顯缺點是 Node.js 中不提供 AbortController,從而在該環境沒有任何優雅或官方的方式來取消異步任務。
正如你在 DOM 規范中所看到的,AbortController 是用一種非常通用的方式描述的。所以你可以在任何類型的異步 API 中使用 —— 甚至是那些目前還不存在的 API。目前只有 Fetch API 正式支持,但是你也可以在自己的代碼中使用它!
在開始之前,讓我們花點時間分析一下 AbortController 的工作原理:
const abortController = new AbortController(); // 1 const abortSignal = abortController.signal; // 2 fetch( 'http://example.com', { signal: abortSignal // 3 } ).catch( ( { message } ) => { // 5 console.log( message ); } ); abortController.abort(); // 4
查看上面的代碼,你會發現在開始時創建了 AbortController DOM 接口的新實例(1),並將其 signal 屬性綁定到變量(2)。然后調用 fetch() 並傳遞 signal 作為其選項之一(3)。要中止獲取資源,你只需調用abortController.abort()(4)。它將自動拒絕 fetch()的 promise,並且控件將傳遞給 catch()塊(5)。
signal 屬性本身非常有趣,它是該節目的主要明星。該屬性是 AbortSignal DOM 接口的實例,該實例具有 aborted 屬性,其中包含有關用戶是否已調用 abortController.abort() 方法的信息。你還可以將 abort 事件偵聽器綁定到將要調用 abortController.abort() 時調用的事件監聽器。換句話說:AbortController 只是 AbortSignal 的公共接口。
廣州設計公司https://www.houdianzi.com 我的007辦公資源網站https://www.wode007.com
可終止函數
假設我們用一個異步函數執行一些非常復雜的計算(例如,異步處理來自大數組的數據)。為簡單起見,示例函數通過先等待五秒鍾然后再返回結果來模擬這一工作:
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>
在上面的代碼中,向按鈕(1)添加一個異步 click 事件偵聽器,並在其中調用 calculate() 函數(2)。五秒鍾后,將顯示帶有結果的警報對話框(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)。
如果用戶在五秒鍾之內再次單擊該按鈕,則將導致調用 abortController.abort() 函數(5)。反過來,這將在你先前傳遞給 calculate() 的 AbortSignal 實例上觸發 abort 事件(6)。
在 abort 事件偵聽器內部,刪除了滴答計時器(7)並拒絕了帶有適當錯誤的promise (8; 根據規范 ,它必須是類型為 'AbortError' 的 DOMException)。該錯誤最終把控制權傳遞給 catch(9)和 finally 塊(10)。
你還應該准備處理如下情況的代碼:
const abortController = new AbortController(); abortController.abort(); calculate( abortController.signal );
在這種情況下,abort 事件將不會被觸發,因為它發生在將信號傳遞給 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,而無需執行任何其他操作。