如何优雅地取消 JavaScript 异步任务


Posted in Javascript onMarch 22, 2020

在程序中处理异步任务通常比较麻烦,尤其是那些不支持取消异步任务的编程语言。所幸的是,JavaScript 提供了一种非常方便的机制来取消异步任务。

中断信号

自从 ES2015 引入了  Promise ,开发者有了取消异步任务的需求,随后推出的一些 Web API 也开始支持异步方案,比如 Fetch API。TC39 委员会(就是制定 ECMAScript 标准的组织)最初尝试定义一套通用的解决方案,以便后续作为 ECMAScript 标准。但是后来讨论不出什么结果来,这个问题也就搁置了。鉴于此,WHATWG (HTML 标准制定组织)另起炉灶,自己搞出一套解决方案,直接在 DOM 标准上引入了 AbortController。这种做法的坏处显而易见,因为它不是语言层面的 ECMAScript 标准,因此 Node.js 平台也就不支持  AbortController 

在 DOM  规范里, AbortController 设计得非常通用,因此事实上你可以用在任何异步 API 中。目前只得到 Fetch API 的官方支持,但你完全可以用在自己的异步代码里。

在开始介绍之前,我们先看下 AbortController 的工作原理:

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的一个公开接口。

可取消的函数

假设有一个执行复杂计算的异步函数,为简单起见,我们就用定时器模拟:

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' 的效果一样。

增加中断异步任务的功能:

{ // 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)。如果它的值是true,calculate()函数应该立即拒绝 Promise,没必要再往下执行了。
到这里我们就实现了一个完整的可取消的异步函数,以后碰到需要处理异步任务的地方就可以派上用场了。

到此这篇关于如何优雅地取消 JavaScript 异步任务的文章就介绍到这了,更多相关JavaScript 取消异步任务内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
jquery UI 1.72 之datepicker
Dec 29 Javascript
js解决弹窗问题实现班级跳转DIV示例
Jan 06 Javascript
jQuery的load()方法及其回调函数用法实例
Mar 25 Javascript
Jquery使用小技巧汇总
Dec 29 Javascript
JavaScript对Json的增删改属性详解
Jun 02 Javascript
vue2.0实战之基础入门(1)
Mar 27 Javascript
利用chrome浏览器进行js调试并找出元素绑定的点击事件详解
Jan 30 Javascript
javascript事件监听与事件委托实例详解
Aug 16 Javascript
vue-mugen-scroll组件实现pc端滚动刷新
Aug 16 Javascript
layui type2 通过url给iframe子页面传值的例子
Sep 06 Javascript
解决vue项目input输入框双向绑定数据不实时生效问题
Aug 05 Javascript
vue在图片上传的时候压缩图片
Nov 18 Vue.js
Vue-cli3多页面配置详解
Mar 22 #Javascript
redux处理异步action解决方案
Mar 22 #Javascript
JS+CSS实现3D切割轮播图
Mar 21 #Javascript
vue-autoui自匹配webapi的UI控件的实现
Mar 20 #Javascript
jQuery实现中奖播报功能(让文本滚动起来) 简单设置数值即可
Mar 20 #jQuery
微信小程序保持session会话的方法
Mar 20 #Javascript
微信小程序后端无法保持session的原因及解决办法问题
Mar 20 #Javascript
You might like
php实现按文件名搜索文件的远程文件查找器
2014/05/10 PHP
php遍历删除整个目录及文件的方法
2015/03/13 PHP
Laravel5.3+框架定义API路径取消CSRF保护方法详解
2020/04/06 PHP
JavaScript中几个重要的属性(this、constructor、prototype)介绍
2013/05/19 Javascript
jQuery实现列表自动循环滚动鼠标悬停时停止滚动
2013/09/06 Javascript
document.getElementById获取控件对象为空的解决方法
2013/11/20 Javascript
浅析jQuery Ajax通用js封装
2016/06/22 Javascript
Extjs gridpanel 中的checkbox(复选框)根据某行的条件不能选中的解决方法
2017/02/17 Javascript
nodejs中模块定义实例详解
2017/03/18 NodeJs
jQuery封装placeholder效果实现方法,让低版本浏览器支持该效果
2017/07/08 jQuery
你应该了解的JavaScript Array.map()五种用途小结
2018/11/14 Javascript
vue项目中使用bpmn-自定义platter的示例代码
2020/05/11 Javascript
JS变量提升及函数提升实例解析
2020/09/03 Javascript
JS实现鼠标移动拖尾
2020/12/27 Javascript
[51:50]完美世界DOTA2联赛 Magma vs GXR 第一场 11.07
2020/11/10 DOTA
[54:26]完美世界DOTA2联赛PWL S3 Forest vs Rebirth 第一场 12.10
2020/12/12 DOTA
python通过正则查找微博@(at)用户的方法
2015/03/13 Python
python在Windows下安装setuptools(easy_install工具)步骤详解
2016/07/01 Python
Python 实现网页自动截图的示例讲解
2018/05/17 Python
python Cartopy的基础使用详解
2020/11/01 Python
解决pycharm不能自动保存在远程linux中的问题
2021/02/06 Python
涂鸦板简单实现 Html5编写属于自己的画画板
2016/07/05 HTML / CSS
加拿大领先的牛仔零售商:Bluenotes
2018/01/22 全球购物
Linux面试经常问的文件系统操作命令
2015/11/05 面试题
爱国主义教育活动总结
2014/05/07 职场文书
小学教师培训方案
2014/06/09 职场文书
家庭困难证明
2014/10/12 职场文书
教师学习党的群众路线教育实践活动心得体会
2014/10/31 职场文书
2014年会计工作总结
2014/11/27 职场文书
构建和谐校园倡议书
2015/01/19 职场文书
2016中秋节问候语
2015/11/11 职场文书
浅析InnoDB索引结构
2021/04/05 MySQL
解决Golang中ResponseWriter的一个坑
2021/04/27 Golang
Python数据类型最全知识总结
2021/05/31 Python
python基础之//、/与%的区别详解
2022/06/10 Python
IDEA中sout快捷键无效问题的解决方法
2022/07/23 Java/Android