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

处理可能超时的异步操作

时间:2023-03-26 22:36:17 JavaScript

自从ECMAScript的PromiseES2015和async/awaitES2017特性发布以来,异步已经成为前端世界中特别常见的操作。异步代码和同步代码在处理问题的顺序上存在一些差异。编写异步代码需要与编写同??步代码不同的“意识”。为此,我还专门写了一篇《异步编程需要“意识”》,但看的人不多,可能确实会“无聊”。本文要讨论的问题可能还是很“无聊”,但却很现实——如果一段代码长时间无法执行会怎样?如果这是同步代码,我们会看到一种称为“无响应”的现象,或者通俗地说-“死亡”;但如果它是一段异步代码呢?也许我们已经等不及结果了,但是其他的代码就好像这件事没有发生过一样继续。当然,事情并不是真的没有发生,只是不同的情况会产生不同的现象而已。例如,带有加载动画的页面似乎一直在加载;另一个例子是一个页面应该用数据更新,但是数据变化是看不到的;再比如对话框无法关闭……这些现象统称为BUG。但也有某个异步操作进程不“呼应”,就默默死在那里的时候。谁也不知道,页面刷新之后,就再也没有痕迹了。当然,这不是小说,还得谈“生意”。axios自带超时处理使用axios调用WebApi是一种常见的异步操作流程。通常我们的代码会这样写:try{constres=awaitaxios.get(url,options);//TODO用于后续正常业务}catch(err){//TODO用于容错处理,否则报错}这段代码是通用的,所有情况下,执行的都很好,直到有一天用户抱怨:Why是不是等了很久没有反应?然后开发人员意识到,由于服务器压力增大,很难瞬间响应这个请求。考虑到用户的感受,增加了加载动画:try{showLoading();constres=awaitaxios.get(url,options);//TODO正常业务}catch(err){//TODO容错处理}finally{hideLoading();}然而有一天,一位用户说:“我等了半个小时,一直在兜圈子!“所以开发人员意识到由于某种原因,请求被卡住了。在这种情况下,应该重新发送请求,或者直接向用户报告——好吧,应该添加超时检查。好在axios确实可以处理超时,只需要在options中加上timeout:3000就可以解决问题。如果超时,可以在catch块中检测并处理:")){//如果axios的请求错误,消息是延迟消息//TODO处理超时}}finally{...}axios好了,如果用fetch()呢?处理fetch()超时fetch()本身不具备处理超时的能力,我们需要在判断超时后通过AbortController触发“取消”请求操作。如果您需要中止fetch()操作,只需从AbortController对象获取信号并将该信号对象作为选项传递给fetch()。大概是这样的:constac=newAbortController();const{信号}=ac;fetch(url,{signal}).then(res=>{//TODO处理业务});//1秒后取消fetch操作setTimeout(()=>ac.abort(),1000);ac.abort()会向signal发送信号,触发其abort事件,并将其.aborted属性设置为true。fetch()的内部处理使用此信息来中止请求。上面的示例演示了如何为fetch()操作实现超时。如果使用await来处理,需要把setTimeout(...)放在fetch(...)之前:constac=newAbortController();const{signal}=ac;setTimeout(()=>ac.abort(),1000);constres=awaitfetch(url,{signal}).catch(()=>undefined);为了避免使用try...catch...来处理请求失败,这里在fetch()之后添加了一个.catch(...)来忽略错误情况。如果发生错误,res将被赋值为undefined。实际业务处理可能需要更合理的catch()处理,让res包含可识别的错误信息。到这里就可以结束了,但是每次fetch()调用都写这么长的代码会很麻烦。最好封装一下:asyncfunctionfetchWithTimeout(timeout,resource,init={}){constac=newAbortController();constsignal=ac.signal;setTimeout(()=>ac.abort(),超时);returnfetch(resource,{...init,signal});}好吗?不,有问题。如果我们在上面代码的setTimeout(...)中输出一条信息:setTimeout(()=>{console.log("It'stimeout");ac.abort();},timeout);并在足够的时间调用:fetchWithTimeout(5000,url).then(res=>console.log("success"));我们会看到输出成功,5秒后我们会看到输出It'stimeout。顺便说一句,虽然我们处理了fetch(...)的超时,但如果fetch(...)成功,我们并没有终止计时器。一个有思想的程序员怎么会犯这样的错误呢?杀了他!异步函数fetchWithTimeout(timeout,rescue,init={}){constac=newAbortController();constsignal=ac.signal;consttimer=setTimeout(()=>{console.log("超时了");returnac.abort();},timeout);尝试{returnawaitfetch(resource,{...init,signal});}最后{clearTimeout(timer);}}完美的!但问题还没有结束。万物皆可超时Axios和fetch都提供了中断异步操作的方式,但是一个没有abort能力的普通Promise呢?对于这样的诺言,我只能说,放过他吧,让他干很久——反正我也拦不住。但是生活还要继续,我不能永远等下去!这种情况下,我们可以把setTimeout()封装成一个Promise,然后用Promise.race()来实现“超时”:race就是赛跑的意思,那么Promise.race()的行为好理解吗??functionwaitWithTimeout(promise,timeout,timeoutMessage="timeout"){让定时器;consttimeoutPromise=newPromise((_,reject)=>{timer=setTimeout(()=>reject(timeoutMessage),超时);});返回Promise.race([timeoutPromise,promise]).finally(()=>clearTimeout(timer));//不要忘记清除定时器}可以写一个Timeout来模拟效果:(async()=>{constbusiness=newPromise(resolve=>setTimeout(resolve,1000*10));try{awaitwaitWithTimeout(business,1000);console.log("[Success]");}catch(err){console.log("[Error]",err);//[Error]超时}})();至于如何写可以中止的异步操作,下次再说。