1. 程式人生 > 實用技巧 >怎樣取消 JavaScript 中的非同步任務?

怎樣取消 JavaScript 中的非同步任務?

中止訊號(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

檢視上面的程式碼,你會發現在開始時建立了AbortControllerDOM 介面的新例項(1),並將其signal屬性繫結到變數(2)。然後呼叫fetch()並傳遞signal作為其選項之一(3)。要中止獲取資源,你只需呼叫abortController.abort()(4)。它將自動拒絕fetch()的 promise,並且控制元件將傳遞給catch()塊(5)。

signal屬性本身非常有趣,它是該節目的主要明星。該屬性是 AbortSignalDOM 介面的例項,該例項具有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,而無需執行任何其他操作。