怎樣取消 JavaScript 中的異步任務

做者:Tomasz Jakut

翻譯:瘋狂的技術宅javascript

原文:https://ckeditor.com/blog/Abo...html

未經容許嚴禁轉載前端

有時候執行異步任務多是很困難的,尤爲是在特定的編程語言不容許取消被錯誤啓動或再也不須要的操做時。幸運的是 JavaScript 提供了很是方便的功能來停止異步活動。在本文中,你能夠學到如何建立可停止的函數。java

停止信號(Abort signal)

在將 Promise 引入 ES2015 並出現了一些支持新異步解決方案的 Web API 以後不久,須要取消異步任務的需求就出現了。最初的嘗試集中在建立通用解決方案上,並期待之後能夠成爲 ECMAScript 標準的一部分。可是,討論很快陷入僵局,沒法解決問題。所以,WHATWG 準備了本身的解決方案,並AbortController 的形式將其直接引入 DOM。這種解決方案的明顯缺點是 Node.js 中不提供 AbortController,從而在該環境沒有任何優雅或官方的方式來取消異步任務。git

正如你在 DOM 規範中所看到的,AbortController 是用一種很是通用的方式描述的。因此你能夠在任何類型的異步 API 中使用 —— 甚至是那些目前還不存在的 API。目前只有 Fetch API 正式支持,可是你也能夠在本身的代碼中使用它!程序員

在開始以前,讓咱們花點時間分析一下 AbortController 的工做原理:es6

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)。github

signal 屬性自己很是有趣,它是該節目的主要明星。該屬性是 AbortSignal DOM 接口的實例,該實例具備 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>

在上面的代碼中,向按鈕(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,而無需執行任何其餘操做。

這就是建立徹底可停止的異步函數的方式。 演示可在這裏得到(https://blog.comandeer.pl/ass...)。請享用!


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎繼續閱讀本專欄其它高贊文章:


相關文章
相關標籤/搜索