如何优雅地取消 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译站
相关文章
相关标签/搜索