如何优雅地取消 JavaScript 异步任务
在程序中处理异步任务通常比较麻烦,尤其是那些不支持取消异步任务的编程语言。所幸的是,JavaScript提供了一种非常方便的机制来取消异步任务。
中断信号
自从ES2015引入了 Promise ,开发者有了取消异步任务的需求,随后推出的一些WebAPI也开始支持异步方案,比如FetchAPI。TC39委员会(就是制定ECMAScript标准的组织)最初尝试定义一套通用的解决方案,以便后续作为ECMAScript标准。但是后来讨论不出什么结果来,这个问题也就搁置了。鉴于此,WHATWG(HTML标准制定组织)另起炉灶,自己搞出一套解决方案,直接在DOM标准上引入了 AbortController。这种做法的坏处显而易见,因为它不是语言层面的ECMAScript标准,因此Node.js平台也就不支持 AbortController 。
在DOM 规范里, AbortController 设计得非常通用,因此事实上你可以用在任何异步API中。目前只得到FetchAPI的官方支持,但你完全可以用在自己的异步代码里。
在开始介绍之前,我们先看下 AbortController 的工作原理:
constabortController=newAbortController();//1 constabortSignal=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的一个公开接口。
可取消的函数
假设有一个执行复杂计算的异步函数,为简单起见,我们就用定时器模拟:
functioncalculate(){ returnnewPromise((resolve,reject)=>{ setTimeout(()=>{ resolve(1); },5000); }); } calculate().then((result)=>{ console.log(result); });
可能的情况是,用户想取消这种耗时的任务。我们用一个按钮来开始和停止:
Calculate document.querySelector('#calculate').addEventListener('click',async({target})=>{//1 target.innerText='Stopcalculation'; constresult=awaitcalculate();//2 alert(result);//3 target.innerText='Calculate'; }); functioncalculate(){ returnnewPromise((resolve,reject)=>{ setTimeout(()=>{ resolve(1); },5000); }); }
上面的代码给按钮绑定了一个异步的 click 事件处理器(1),并在里面调用了 calculate() 函数(2)。5秒后会弹出对话框显示结果(3)。顺便提一下,script[type=module]可以让JavaScript代码进入严格模式,跟'usestrict'的效果一样。
增加中断异步任务的功能:
{//1 letabortController=null;//2 document.querySelector('#calculate').addEventListener('click',async({target})=>{ if(abortController){ abortController.abort();//5 abortController=null; target.innerText='Calculate'; return; } abortController=newAbortController();//3 target.innerText='Stopcalculation'; try{ constresult=awaitcalculate(abortController.signal);//4 alert(result); }catch{ alert('WHYDIDYOUDOTHAT?!');//9 }finally{//10 abortController=null; target.innerText='Calculate'; } }); functioncalculate(abortSignal){ returnnewPromise((resolve,reject)=>{ consttimeout=setTimeout(()=>{ resolve(1); },5000); abortSignal.addEventListener('abort',()=>{//6 consterror=newDOMException('Calculationabortedbytheuser','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)。
但是还要考虑这样一种情况:
constabortController=newAbortController(); abortController.abort(); calculate(abortController.signal);
这种情况下 abort 事件不会触发,因为它在signal传给calculate() 函数前就执行了。为此我们需要改造下代码:
functioncalculate(abortSignal){ returnnewPromise((resolve,reject)=>{ consterror=newDOMException('Calculationabortedbytheuser','AbortError');//1 if(abortSignal.aborted){//2 returnreject(error); } consttimeout=setTimeout(()=>{ resolve(1); },5000); abortSignal.addEventListener('abort',()=>{ clearTimeout(timeout); reject(error); }); }); }
异常对象的定义移到了顶部(1),这样就可以在两个地方重用了。另外,多了个条件判断abortSignal.aborted(2)。如果它的值是true,calculate()函数应该立即拒绝Promise,没必要再往下执行了。
到这里我们就实现了一个完整的可取消的异步函数,以后碰到需要处理异步任务的地方就可以派上用场了。
到此这篇关于如何优雅地取消JavaScript异步任务的文章就介绍到这了,更多相关JavaScript取消异步任务内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!