当前位置: 首页 > Web前端 > JavaScript

可中断的异步操作

时间:2023-03-26 22:55:53 JavaScript

前面我们讲到可能超时的异步操作,其中提到了fetch()异步操作的“中断”处理。这一次,我们来谈谈“中断”异步操作。由于JavaScript的单线程特性,JavaScript中可以执行的异步场景并不多。大概有以下几种:setTimeout()/setInterval()事件Ajax部分调用Native方法...中断Ajax操作Ajax处理也基本可以归类为“调用Native方法”,因为它基本上是通过浏览器提供的XMLHttpRequest或fetch()。所以axios、fetch()、jQuery.ajax()都提供了abort接口。中断fetch()在《可能超时的异步操作》中已经有例子,这里再举一个jQuery和Axios的例子。jQuery的jqXHR提供.abort()//url是beeceptor.com上做的GET接口,需要3秒后响应constfetching=$.ajax(url,{type:"get"}).done(()=>console.log("看不到这句话")).fail(()=>console.log("但是可以看到这句话"));setTimeout(()=>fetching.abort(),1000);//1秒后的中断请求也可以写成await的方式:(async()=>{try{constfetching=$.ajax(url,{type:"get"});setTimeout(()=>fetching.abort(),1000);awaitfetching;console.log("看不到这句话");}catch(err){console.log("但是可以看到这句话");}})();中断axios请求axios提供CancelToken来实现中断。这种模式和上一篇中断fetch()的AbortController和AbortSignal是一样的。//在Node中需要导入;浏览器中直接引用的axios.js会有一个全局的axios对象importaxiosfrom"Axios";(async()=>{const{CancelToken}=axios;constsource=CancelToken.source();//创建中断源try{setTimeout(()=>source.cancel("1秒中断"),1000);constdata=awaitaxios.get(url,//在beeceptor.com上做的需要3秒后响应的GET接口{cancelToken:source.token//传入token});console.log("因为超时中断看不到这句话");}catch(err){if(axios.isCancel(err)){console.log("axios请求被超时中断",err);//axios请求被超时中断Cancel{message:'1secondinterruption'}}else{console.log("Anothererroroccurred");}}})();中断定时器和事件到setTiemout()/setInteraval()可以说是比较简单的,使用clearTimeout()/clearInterval()就可以搞定。和中断事件——直接注销事件处理函数。但需要注意的是,有些事件框架需要提供注销事件时注册的事件处理函数。比如removeEventListener()需要提供原处理函数;而jQuery通过.off()取消事件处理功能只需要提供名称和命名空间(如果有的话)。但是,当这些过程被封装在Promise中时,记得在处理“注销”时拒绝(当然,如果你同意resolve一个特殊的值)。以setTimeout为例:asyncfunctiondelayToDo(){returnnewPromise((resolve,reject)=>{consttimer=setTimeout(()=>{resolve("延迟后获取此文本");},5000);setTimeout(()=>{clearTimeout(timer);reject("Timedout");},1000);});}可以说这段代码相当没用——谁会设置一个延迟立即设置一个更短的超时任务后?带出一个abort()函数如果我们需要设置一个延迟任务,在某些情况下中断它,正确的做法是把定时器从延迟任务函数中取出来,用在别处。更好的方法是带出一个abort()函数,使语义更准确。函数delayToDo(ms){让计时器;constpromise=newPromise(resolve=>{timer=setTimeout(()=>{resolve("延迟后获取此文本");},ms);});promise.abort=()=>clearTimeout(timer);returnpromise;}constpromise=delayToDo(5000);//在其他业务逻辑中,使用promise.abort()中断延迟任务setTimeout(()=>promise.abort(),1000);使用传输框对象将abort()发送出去。请注意delayToDo()不是异步函数。如果我们使用异步修改,我们将无法获得返回承诺。如果你真的需要使用async来修改它,你必须解决它并通过“中转箱”对象将abort()带出。functiondelayToDo(ms,transferBox){returnnewPromise((resolve,reject)=>{consttimer=setTimeout(()=>{resolve("Getthistextafterdelay");},ms);//如果有是一个中转盒,发出abort函数transferbox对象,注意作用域(所以定义在IIFE之外)constbox={};(async()=>{try{consts=awaitdelayToDo(5000,box);console.log("不会输出这句话",s);}catch(err){console.log("错误",err);}})();//1秒后,延时操作会被传出的abort打断setTimeout(()=>box.abort("timeoutinterrupt"),1000);//1秒后,下面一行会输出//Error{abort:true,message:'timeoutinterrupt'}UseAbortController&AbortSignal就是使用中转盒的操作,看起来和axios的CancelToken很像。只是CancelToken把信号带进了异步操作,中转盒把中断函数带进了外面。AbortController和CanelToken的原理是类似的。现代环境(Chrome66+、Nodejs15+)具有AbortController。你不妨尝试使用这个专业的工具类。functiondelayToDo(ms,signal){returnnewPromise((resolve,reject)=>{consttimer=setTimeout(()=>resolve("Getthistextafterdelay"),ms);if(signal){//如果AbortController发出中断信号,就会触发onabort事件signal.onabort=()=>{clearTimeout(timer);reject({abort:true,message:"timeout"});};}});}constabortController=newAbortController();(async()=>{try{consts=awaitdelayToDo(5000,abortController.signal);console.log("这句话不会输出",s);}catch(err){console.log("Error",err);}})();setTimeout(()=>abortController.abort(),1000);这段代码其实和上面的没有太大区别,只是在使用了AbortController之后语义更加明确了一点。毕竟是专门用来做“打断”的。但遗憾的是,AbortController的abort()方法没有带任何参数,也不能带入中断消息(reason)。实现一个MyAbortAbortController还在实验阶段,还不是很成熟,所以有一些不尽人意是正常的。不过这个原理说起来并不难,你不妨自己实现一个。这段JavaScript代码使用了ESM语法、Privatefield、Fielddeclarations、Symbol等,不明白的请查看MDN。constABORT=Symbol("abort");exportclassMyAbortSingal{#onabort;中止;共鸣;//使用模块中未导出的ABORTSymbol来定义,有两个目的//1)避免被用户调用//2)调用MyAbort(如果做成私有字段,MyAbort无法访问)[ABORT](reson){this.reson=共鸣;this.aborted=true;如果(this.#onabort){this.#onabort(reson);}}//允许设置onabort,但不获取它(并且不需要获取它)setonabort(fn){if(typeoffn==="function"){this.#onabort=fn;}}}exportclassMyAbort{#signal;constructor(){this.#signal=newMyAbortSingal();}//允许获取信号,但不允许设置getsignal(){returnthis.#signal;}abort(reson){this.#signal[ABORT](reson);}}使用MyAbort直接替换前面示例代码中的AbortController。并且在调用.abort()时,也可以传入reason。修改后的代码如下:import{MyAbort}from"./my-abort.js";functiondelayToDo(ms,signal){returnnewPromise((resolve,reject)=>{...reject({abort:true,message:signal.reson});...});}constabortController=newMyAbort();...setTimeout(()=>abortController.abort("一秒超时"),1000);更详细的中断对于定时器和事件,主要使用“注销”方法来中断。但实际上这个粒度可能有点粗。打破无限循环如果有一件事,你需要不断尝试,直到成功。这种东西一般写成死循环,直到达到目的才会跳出循环。如果不是JavaScript,比如Java或者C#,一般都是新开一个线程来做,然后每次循环检查,看有没有abort信号,有就中断。JavaScript是单线程的,你要写死循环,那你就真的死定了。但是有一个解决方法——使用setInterval()定期处理它,就像循环一样,每隔一段时间处理一次,直到你使用clearInterval()结束它(就像退出循环)。与循环一样,可以在周期性处理时判断中止信号,像这样:/TODO业务处理},200);signal.onabort=()=>clearInterval(timer);}constac=newAbortController();loop(ac.signal);不是真的死了,中断接口还留着呢。中断复杂的多步异步操作除了循环之外,还有一些异步操作需要大量的时间。例如,处理某项业务需要与后台进行多次交互:通过用户输入的信息进行鉴权。获得认证后,获取用户的基本信息。从用户信息的部门编号中获取部门信息。根据部门信息获取部门的相关信息。这里举例的业务操作,步骤非常多,其实可以通过跟后台协商来简化,但不在我们今天的讨论范围之内。在实际业务中,确实有很多情况需要经过多个步骤才能完成。我们现在要讨论的是如何打断。先看这个业务流程的示例代码:asyncfunctionlongBusiness(){constauth=awaitremoteAuth();constuserInfo=awaitfetchUserInfo(auth.token);constdepartment=awaitfetchDepartment(userInfo.departmentId);constdata=awaitfetchData(部门);dealWithData();}语句不多,但是比较耗时。如果交互需要1秒,则操作至少需要4秒才能完成。如果用户想在2秒时中断怎么办?其实和上面的setInterval()是一样的,只是适当插入对abort信号的检查:asyncfunctionsleep(ms){returnnewPromise(resolve=>setTimeout(()=>{console.log(`Complete${ms}task`);resolve();},ms));}//模拟异步函数constremoteAuth=()=>sleep(1000);constfetchUserInfo=()=>sleep(2000);constfetchDepartment=()=>sleep(3000);constfetchData=()=>睡眠(4000);asyncfunctionlongBusiness(signal){try{constauth=awaitremoteAuth();检查中止();constuserInfo=awaitfetchUserInfo(auth?.token);检查中止();constdepartment=awaitfetchDepartment(userInfo?.departmentId);检查中止();constdata=awaitfetchData(部门);检查中止();//TODO处理数据}catch(err){if(err===signal){console.log("Abortexit");返回;}//其他情况是业务错误,应该进行容错处理,或者抛给外层逻辑处理throwerr;}功能检查中止(){if(signal.aborted){//抛出的函数可以在catch中检查,最好定义一个AbortError抛出信号;}}}constac=newAbortController();longBusiness(ac.signal);setTimeout(()=>{ac.abort();},2000);每次执行耗时操作时,longBusiness()都会执行中止信号检查。例子中如果检测到abort信息,就会抛出异常中断程序。通过抛出异常来中断程序更方便。如果不喜欢,也可以用ifbranch来处理,比如if(signal.aborted){return;}.这个示例程序会完成两个耗时任务,因为当请求中断时,第二个耗时任务正在进行中,直到结束才会进行下一次中止信息检查。总结一般来说,中断并不难。但是我们在编写程序的时候,往往会忘记进行耗时程序可能需要的中断处理。必要的中断处理可以节省计算资源,提高用户体验。如果你有合适的业务场景,不妨试试看!