如何取消 JavaScript 中的異步任務
瘋狂的技術宅 前端先鋒
翻譯:瘋狂的技術宅
作者:Tomasz Jakut
來源:ckeditor.com
正文共:2407 字
預計閱讀時間:7 分鐘
有時候執行異步任務可能是很困難的,尤其是在特定的編程語言不允許取消被錯誤啓動或不再需要的操作時。幸運的是 JavaScript 提供了非常方便的功能來中止異步活動。在本文中,你可以學到如何創建可中止的函數。
中止信號(Abort signal)
在將 Promise 引入 ES2015 並出現了一些支持新異步解決方案的 Web API 之後不久,需要取消異步任務的需求就出現了(https://github.com/whatwg/fetch/issues/27)。最初的嘗試集中在創建通用解決方案(https://github.com/tc39/proposal-cancellation)上,並期待以後可以成爲 ECMAScript 標準的一部分。但是,討論很快陷入僵局,無法解決問題。因此,WHATWG 準備了自己的解決方案,並以 AbortController 的形式將其直接引入 DOM。這種解決方案的明顯缺點是 Node.js 中不提供 AbortController,從而在該環境沒有任何優雅或官方的方式來取消異步任務。
正如你在 DOM 規範中所看到的,AbortController 是用一種非常通用的方式描述的。所以你可以在任何類型的異步 API 中使用 —— 甚至是那些目前還不存在的 API。目前只有 Fetch API 正式支持,但是你也可以在自己的代碼中使用它!
在開始之前,讓我們花點時間分析一下 AbortController 的工作原理:
1const abortController = new AbortController(); // 1
2const abortSignal = abortController.signal; // 2
3
4fetch( 'http://example.com', {
5 signal: abortSignal // 3
6} ).catch( ( { message } ) => { // 5
7 console.log( message );
8} );
9
10abortController.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 的公共接口。
可終止函數
假設我們用一個異步函數執行一些非常複雜的計算(例如,異步處理來自大數組的數據)。爲簡單起見,示例函數通過先等待五秒鐘然後再返回結果來模擬這一工作:
1function calculate() {
2 return new Promise( ( resolve, reject ) => {
3 setTimeout( ()=> {
4 resolve( 1 );
5 }, 5000 );
6 } );
7}
8
9calculate().then( ( result ) => {
10 console.log( result );
11} );
但有時用戶希望能夠中止這種代價高昂的操作。沒錯,他們應該有這樣的能力。添加一個能夠啓動和停止計算的按鈕:
1<button id="calculate">Calculate</button>
2
3<script type="module">
4 document.querySelector( '#calculate' ).addEventListener( 'click', async ( { target } ) => { // 1
5 target.innerText = 'Stop calculation';
6
7 const result = await calculate(); // 2
8
9 alert( result ); // 3
10
11 target.innerText = 'Calculate';
12 } );
13
14 function calculate() {
15 return new Promise( ( resolve, reject ) => {
16 setTimeout( ()=> {
17 resolve( 1 );
18 }, 5000 );
19 } );
20 }
21</script>
在上面的代碼中,向按鈕(1)添加一個異步 click 事件偵聽器,並在其中調用 calculate() 函數(2)。五秒鐘後,將顯示帶有結果的警報對話框(3)。另外, script [type = module] 用於強制 JavaScript 代碼進入嚴格模式——因爲它比 'use strict' 編譯指示更爲優雅。
現在添加中止異步任務的功能:
1{ // 1
2 let abortController = null; // 2
3
4 document.querySelector( '#calculate' ).addEventListener( 'click', async ( { target } ) => {
5 if ( abortController ) {
6 abortController.abort(); // 5
7
8 abortController = null;
9 target.innerText = 'Calculate';
10
11 return;
12 }
13
14 abortController = new AbortController(); // 3
15 target.innerText = 'Stop calculation';
16
17 try {
18 const result = await calculate( abortController.signal ); // 4
19
20 alert( result );
21 } catch {
22 alert( 'WHY DID YOU DO THAT?!' ); // 9
23 } finally { // 10
24 abortController = null;
25 target.innerText = 'Calculate';
26 }
27 } );
28
29 function calculate( abortSignal ) {
30 return new Promise( ( resolve, reject ) => {
31 const timeout = setTimeout( ()=> {
32 resolve( 1 );
33 }, 5000 );
34
35 abortSignal.addEventListener( 'abort', () => { // 6
36 const error = new DOMException( 'Calculation aborted by the user', 'AbortError' );
37
38 clearTimeout( timeout ); // 7
39 reject( error ); // 8
40 } );
41 } );
42 }
43}
如你所見,代碼變得更長了。但是沒有理由驚慌,它並沒有變得更難理解!
一切都包含在塊(1)中,該塊相當於 IIFE (https://exploringjs.com/es6/ch_core-features.html#sec_from-iifes-to-blocks)。因此,abortController 變量(2)不會泄漏到全局作用域內。
首先,將其值設置爲 null 。鼠標單擊按鈕時,此值會更改。然後將其值設置爲 AbortController 的新實例(3)。之後,將實例的 signal 屬性直接傳遞給你的 calculate() 函數(4)。
如果用戶在五秒鐘之內再次單擊該按鈕,則將導致調用 abortController.abort() 函數(5)。反過來,這將在你先前傳遞給 calculate() 的 AbortSignal 實例上觸發 abort 事件(6)。
在 abort 事件偵聽器內部,刪除了滴答計時器(7)並拒絕了帶有適當錯誤的promise (8; 根據規範(https://dom.spec.whatwg.org/#abortcontroller-api-integration) ,它必須是類型爲 'AbortError' 的 DOMException)。該錯誤最終把控制權傳遞給 catch(9)和 finally 塊(10)。
你還應該準備處理如下情況的代碼:
1const abortController = new AbortController();
2
3abortController.abort();
4calculate( abortController.signal );
在這種情況下,abort 事件將不會被觸發,因爲它發生在將信號傳遞給 calculate() 函數之前。因此你應該進行一些重構:
1function calculate( abortSignal ) {
2 return new Promise( ( resolve, reject ) => {
3 const error = new DOMException( 'Calculation aborted by the user', 'AbortError' ); // 1
4
5 if ( abortSignal.aborted ) { // 2
6 return reject( error );
7 }
8
9 const timeout = setTimeout( ()=> {
10 resolve( 1 );
11 }, 5000 );
12
13 abortSignal.addEventListener( 'abort', () => {
14 clearTimeout( timeout );
15 reject( error );
16 } );
17 } );
18}
錯誤被移到頂部(1)。因此,你可以在代碼不同部分中重用它(但是,創建一個錯誤工廠會更優雅,儘管聽起來很愚蠢)。另外出現了一個保護子句,檢查 abortSignal.aborted(2)的值。如果等於 true,那麼 calculate() 函數將會拒絕帶有適當錯誤的 promise,而無需執行任何其他操作。
這就是創建完全可中止的異步函數的方式。演示可在這裏獲得(https://blog.comandeer.pl/assets/i-ciecie/)。請享用!
原文鏈接
https://ckeditor.com/blog/Aborting-a-signal-how-to-cancel-an-asynchronous-task-in-JavaScript/