如何取消 JavaScript 中的异步任务
疯狂的技术宅 前端先锋
翻译:疯狂的技术宅
作者:Tomasz Jakut
来源:ckeditor.com
正文共:2407 字
预计阅读时间:7 分钟
有时候执行异步任务可能是很困难的,尤其是在特定的编程语言不允许取消被错误启动或不再需要的操作时。幸运的是 JavaScript 提供了非常方便的功能来中止异步活动。在本文中,你可以学到如何创建可中止的函数。
中止信号(Abort signal)
在将 Promise 引入 ES2015 并出现了一些支持新异步解决方案的 Web API 之后不久,需要取消异步任务的需求就出现了(https://github.com/whatwg/fetch/issues/27)。最初的尝试集中在创建通用解决方案(https://github.com/tc39/proposal-cancellation)上,并期待以后可以成为 ECMAScript 标准的一部分。但是,讨论很快陷入僵局,无法解决问题。因此,WHATWG 准备了自己的解决方案,并以 AbortController 的形式将其直接引入 DOM。这种解决方案的明显缺点是 Node.js 中不提供 AbortController,从而在该环境没有任何优雅或官方的方式来取消异步任务。
正如你在 DOM 规范中所看到的,AbortController 是用一种非常通用的方式描述的。所以你可以在任何类型的异步 API 中使用 —— 甚至是那些目前还不存在的 API。目前只有 Fetch API 正式支持,但是你也可以在自己的代码中使用它!
在开始之前,让我们花点时间分析一下 AbortController 的工作原理:
1const abortController = new AbortController(); // 1
2const abortSignal = abortController.signal; // 2
3
4fetch( 'http://example.com', {
5 signal: abortSignal // 3
6} ).catch( ( { message } ) => { // 5
7 console.log( message );
8} );
9
10abortController.abort(); // 4
查看上面的代码,你会发现在开始时创建了 AbortController DOM 接口的新实例(1),并将其 signal 属性绑定到变量(2)。然后调用 fetch() 并传递 signal 作为其选项之一(3)。要中止获取资源,你只需调用abortController.abort()(4)。它将自动拒绝 fetch()的 promise,并且控件将传递给 catch()块(5)。
signal 属性本身非常有趣,它是该节目的主要明星。该属性是 AbortSignal DOM 接口的实例,该实例具有 aborted 属性,其中包含有关用户是否已调用 abortController.abort() 方法的信息。你还可以将 abort 事件侦听器绑定到将要调用 abortController.abort() 时调用的事件监听器。换句话说:AbortController 只是 AbortSignal 的公共接口。
可终止函数
假设我们用一个异步函数执行一些非常复杂的计算(例如,异步处理来自大数组的数据)。为简单起见,示例函数通过先等待五秒钟然后再返回结果来模拟这一工作:
1function calculate() {
2 return new Promise( ( resolve, reject ) => {
3 setTimeout( ()=> {
4 resolve( 1 );
5 }, 5000 );
6 } );
7}
8
9calculate().then( ( result ) => {
10 console.log( result );
11} );
但有时用户希望能够中止这种代价高昂的操作。没错,他们应该有这样的能力。添加一个能够启动和停止计算的按钮:
1<button id="calculate">Calculate</button>
2
3<script type="module">
4 document.querySelector( '#calculate' ).addEventListener( 'click', async ( { target } ) => { // 1
5 target.innerText = 'Stop calculation';
6
7 const result = await calculate(); // 2
8
9 alert( result ); // 3
10
11 target.innerText = 'Calculate';
12 } );
13
14 function calculate() {
15 return new Promise( ( resolve, reject ) => {
16 setTimeout( ()=> {
17 resolve( 1 );
18 }, 5000 );
19 } );
20 }
21</script>
在上面的代码中,向按钮(1)添加一个异步 click 事件侦听器,并在其中调用 calculate() 函数(2)。五秒钟后,将显示带有结果的警报对话框(3)。另外, script [type = module] 用于强制 JavaScript 代码进入严格模式——因为它比 'use strict' 编译指示更为优雅。
现在添加中止异步任务的功能:
1{ // 1
2 let abortController = null; // 2
3
4 document.querySelector( '#calculate' ).addEventListener( 'click', async ( { target } ) => {
5 if ( abortController ) {
6 abortController.abort(); // 5
7
8 abortController = null;
9 target.innerText = 'Calculate';
10
11 return;
12 }
13
14 abortController = new AbortController(); // 3
15 target.innerText = 'Stop calculation';
16
17 try {
18 const result = await calculate( abortController.signal ); // 4
19
20 alert( result );
21 } catch {
22 alert( 'WHY DID YOU DO THAT?!' ); // 9
23 } finally { // 10
24 abortController = null;
25 target.innerText = 'Calculate';
26 }
27 } );
28
29 function calculate( abortSignal ) {
30 return new Promise( ( resolve, reject ) => {
31 const timeout = setTimeout( ()=> {
32 resolve( 1 );
33 }, 5000 );
34
35 abortSignal.addEventListener( 'abort', () => { // 6
36 const error = new DOMException( 'Calculation aborted by the user', 'AbortError' );
37
38 clearTimeout( timeout ); // 7
39 reject( error ); // 8
40 } );
41 } );
42 }
43}
如你所见,代码变得更长了。但是没有理由惊慌,它并没有变得更难理解!
一切都包含在块(1)中,该块相当于 IIFE (https://exploringjs.com/es6/ch_core-features.html#sec_from-iifes-to-blocks)。因此,abortController 变量(2)不会泄漏到全局作用域内。
首先,将其值设置为 null 。鼠标单击按钮时,此值会更改。然后将其值设置为 AbortController 的新实例(3)。之后,将实例的 signal 属性直接传递给你的 calculate() 函数(4)。
如果用户在五秒钟之内再次单击该按钮,则将导致调用 abortController.abort() 函数(5)。反过来,这将在你先前传递给 calculate() 的 AbortSignal 实例上触发 abort 事件(6)。
在 abort 事件侦听器内部,删除了滴答计时器(7)并拒绝了带有适当错误的promise (8; 根据规范(https://dom.spec.whatwg.org/#abortcontroller-api-integration) ,它必须是类型为 'AbortError' 的 DOMException)。该错误最终把控制权传递给 catch(9)和 finally 块(10)。
你还应该准备处理如下情况的代码:
1const abortController = new AbortController();
2
3abortController.abort();
4calculate( abortController.signal );
在这种情况下,abort 事件将不会被触发,因为它发生在将信号传递给 calculate() 函数之前。因此你应该进行一些重构:
1function calculate( abortSignal ) {
2 return new Promise( ( resolve, reject ) => {
3 const error = new DOMException( 'Calculation aborted by the user', 'AbortError' ); // 1
4
5 if ( abortSignal.aborted ) { // 2
6 return reject( error );
7 }
8
9 const timeout = setTimeout( ()=> {
10 resolve( 1 );
11 }, 5000 );
12
13 abortSignal.addEventListener( 'abort', () => {
14 clearTimeout( timeout );
15 reject( error );
16 } );
17 } );
18}
错误被移到顶部(1)。因此,你可以在代码不同部分中重用它(但是,创建一个错误工厂会更优雅,尽管听起来很愚蠢)。另外出现了一个保护子句,检查 abortSignal.aborted(2)的值。如果等于 true,那么 calculate() 函数将会拒绝带有适当错误的 promise,而无需执行任何其他操作。
这就是创建完全可中止的异步函数的方式。演示可在这里获得(https://blog.comandeer.pl/assets/i-ciecie/)。请享用!
原文链接
https://ckeditor.com/blog/Aborting-a-signal-how-to-cancel-an-asynchronous-task-in-JavaScript/