怎样取消 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...)。请享用!


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,天天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,天天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


相关文章
相关标签/搜索