如何優雅地取消 JavaScript 異步任務?

在程序中處理異步任務一般比較麻煩,尤爲是那些不支持取消異步任務的編程語言。所幸的是,JavaScript 提供了一種很是方便的機制來取消異步任務。編程

中斷信號

自從 ES2015 引入了  Promise ,開發者有了取消異步任務的需求,隨後推出的一些 Web API 也開始支持異步方案,好比 Fetch API。TC39 委員會(就是制定 ECMAScript 標準的組織)最初嘗試定義一套通用的解決方案,以便後續做爲 ECMAScript 標準。可是後來討論不出什麼結果來,這個問題也就擱置了。鑑於此,WHATWG (HTML 標準制定組織)另起爐竈,本身搞出一套解決方案,直接在 DOM 標準上引入了 AbortController。這種作法的壞處顯而易見,由於它不是語言層面的 ECMAScript 標準,所以 Node.js 平臺也就不支持  AbortController 。bash

在 DOM 規範裏, AbortController 設計得很是通用,所以事實上你能夠用在任何異步 API 中。目前只獲得 Fetch API 的官方支持,但你徹底能夠用在本身的異步代碼裏。微信

在開始介紹以前,咱們先看下 AbortController 的工做原理:dom

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的一個公開接口。async

可取消的函數

假設有一個執行復雜計算的異步函數,爲簡單起見,咱們就用定時器模擬:編程語言

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'的效果同樣。fetch

增長中斷異步任務的功能:ui

{ // 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)。若是它的值是truecalculate()函數應該當即拒絕 Promise,不必再往下執行了。

到這裏咱們就實現了一個完整的可取消的異步函數,之後碰到須要處理異步任務的地方就能夠派上用場了。

動動金手指關注下公衆號1024譯站,祝(助)你少加班~

微信公衆號:1024譯站
相關文章
相關標籤/搜索