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

async-await异步应用的常见场景

时间:2023-03-30 23:50:51 CSS

前言async/await语法以一种看起来像写同步代码的方式优雅地处理异步操作,但我们也必须明白,异步操作本来就是复杂的,像写同步代码的方法不能减少本质的复杂性,所以我们在处理它时必须更加谨慎。如果我们不小心,可能会写出没有按预期执行的代码,从而影响执行效率。下面将简单描述一些常见的日常场景,加深对async/await的理解。最常见的异步操作是请求。我们也可以使用setTimeOut来简单模拟一个异步请求。场景一、一个接一个的请求相信是最常见的场景。后一个请求依赖于前一个请求。下面是一个爬取网页中图片的例子,使用了superagent请求模块和cheerio页面分析模块。图片的地址需要通过分析网页内容得到,所以请求一定要按顺序进行。constrequest=require('superagent')constcheerio=require('cheerio')//简单封装请求,其他类似函数getHTML(url){//一些操作,比如设置请求头信息returnsuperagent.get(url).set('referer',referer).set('user-agent',userAgent)}//在下方请求图片asyncfunctionimageCrawler(url){letres=awaitgetHTML(url)lethtml=res.textlet$=cheerio.load(html)let$img=$(selector)[0]lethref=$img.attribs.srcres=awaitgetImage(href)returnres.body}asyncfunctionhandler(url){letimg=awaitimageCrawler(url)console.log(img)//buffer格式的数据//处理图片}handler(url)上面是一个简单的获取图片数据的场景,图片数据加载到内存中,如果只是简单的数据存储即可以流的形式存储,防止过多的内存消耗。其中,awaitgetHTML是必须的。如果省略await程序,则无法获得预期的结果。执行过程会先执行await之后的表达式,实际上返回的是pending状态的promise。await之后的操作直到promise处于resolved状态才会执行,代码执行会跳出async函数继续执行函数外的其他代码,所以不会阻塞后续代码的执行。场景二、并发请求有时候我们不需要等一个请求回来再发送另一个请求,这样效率很低,所以这时候就需要并发执行请求任务。让我们以查询为例。首先获取一个人的学校地址和家庭住址,然后从这些信息中获取详细的个人信息。学校地址和家庭住址之间没有依赖关系。后续对个人信息的获取依赖于两者的async函数。infoCrawler(url,name){let[schoolAdr,homeAdr]=awaitPromise.all([getSchoolAdr(name),getHomeAdr(name)])letinfo=awaitgetInfo(url+`?schoolAdr=${schoolAdr}&homeAdr=${homeAdr}`)returninfo}上面使用的Promise.all中的异步请求会并发执行,数据准备好后,会按照数据的顺序返回对应的数组。在这里,获取信息的最终处理时间由并发请求决定。最慢的请求决定,比如getSchoolAdr长时间没有返回数据,那么后续操作只能等待,即使getHomeAdr已经提前返回了,当然上面的场景一定要这么干,但是有时候我们不需要这样做。在上面的第一个场景中,我们只得到了一张图片,但是一个网页中的图片可能不止一张。如果我们要存储这些图片,不需要等图片有并发请求回来再处理。哪一个早点回来就存储图片letimageUrls=['href1','href2','href3']asyncfunctionsaveImages(imageUrls){awaitPromise.all(imageUrls.map(asyncimageUrl=>{letimg=awaitgetImage(imageUrl)returnawaitsaveImage(img)}))console.log('done')}//如果我们根本不关心存储是否完整,我们也可以这样写letimageUrls=['href1','href2','href3']//saveImages()甚至保存asyncfunctionsaveImages(imageUrls){imageUrls.forEach(asyncimageUrl=>{letimg=awaitgetImage(imageUrl)saveImage(img)})}有些人可能有疑惑forEach不能用于异步?刚开始接触这个语法的时候就听过这个说法。很明显forEach可以处理异步处理,但是是并发处理,map也是并发处理。这个怎么用主要看你的实际情况。场景取决于您是否对结果感兴趣。场景三、错误处理一个请求可能会遇到各种各样的问题。我们不能保证成功。报错很常见,所以有时候需要处理错误,async/await处理错误也很直观,直接用try/catch抓包就可以了asyncfunctionimageCrawler(url){try{letimg=awaitgetImage(url)returnimg}catch(error){console.log(error)}}//imageCrawler返回一个promise可以这样处理:asyncfunctionimageCrawler(url){letimg=awaitgetImage(url)returnimg}imageCrawler(url).catch(err=>{console.log(err)})有人可能会有疑问,他们应该在每个请求中尝试/捕获。其实在最外层抓就可以了。一些基于中间件的设计喜欢在最外层捕获错误。asyncfunctionctx(next){try{awaitnext()}catch(error){console.log(error)}}场景4.当一个请求发出超时处理后,我们无法确定它什么时候返回,我们不能总是傻等。有时需要设置超时处理函数timeOut(delay){returnnewPromise((resolve,reject)=>{setTimeout(()=>{reject(newError('别等了,别傻了'))},delay)})}asyncfunctionimageCrawler(url,delay){try{letimg=awaitPromise.race([getImage(url),timeOut(delay)])returnimg}catch(error){console.log(error)}}这里使用了Promise.race处理超时,需要注意的是如果超过超时,仍然没有终止请求,只是不会进行后续处理当然不用担心,后续处理会报错并导致重新处理错误信息,因为promise的状态一旦改变,就不会再改变了,否则会被网站拦截或者进程崩溃asyncfunctiongetImages(urls,limit){letrunning=0letrletp=newPromise((resolve,reject)=>{r=resolve})functionrun(){if(running0){running++让url=urls.转移();(async()=>{letimg=awaitgetImage(url)running--console.log(img)if(urls.length===0&&running===0){console.log('done')返回r('done')}else{run()}})()run()//立即达到并发限制}}run()returnawaitp}总结以上列举了一些日常场景处理的代码片段。当遇到比较复杂的场景时,可以结合以上场景进行组合使用。如果场景太复杂,最好的办法是使用相关的异步代码控制库。如果想更好的理解async/await,可以先理解promise和generator。Async/await基本上是生成器函数的语法糖。下面简述其内部原理。functiondelay(time){returnnewPromise((resolve,reject)=>{setTimeout(()=>{resolve(time)},time)})}function*createTime(){让time1=yielddelay(1000)让time2=yielddelay(2000)让time3=yielddelay(3000)console.log(time1,time2,time3)}letiterator=createTime()console.log(iterator.next())console.log(iterator.next(1000))console.log(iterator.next(2000))console.log(iterator.next(3000))//输出{value:Promise{},done:false}{value:Promise{},done:false}{value:Promise{},done:false}100020003000{value:undefined,done:true}可以看出每个value都是一个Promise,手动传入参数next可以设置generator的内部值,这里是手动传入的,我只需要写一个递归函数自动添加即可(){if(!result.done){Promise.resolve(result.value).then(时间=>{result=iterator.next(time)autoRun()}).catch(err=>{result=iterator.throw(err)autoRun()})}}autoRun()}run(createTime)promise.resove保证返回除了next方法和throw方法之外,还使用了一个promise对象可迭代对象来将错误传递给生成器。只要能在生成器内部捕获到对象,生成器就可以继续运行,类似下面的代码functiondelay(time){returnnewPromise((resolve,reject)=>{setTimeout(()=>{if(time==2000){reject('2000error')}resolve(time)},time)})}function*createTime(){lettime1=yielddelay(1000)lettime2try{time2=yielddelay(2000)}catch(error){time2=error}lettime3=yielddelay(3000)console.log(time1,time2,time3)}OK可以看出generator函数其实和async/await的语法很像,只需将async/await代码片段更改为生成器函数asyncfunctioncreateTime(){lettime1=awaitdelay(1000)lettime2try{time2=awaitdelay(2000)}catch(error){time2=error}lettime3=awaitdelay(3000)console.log(time1,time2,time3)}functiontransform(async){letstr=async.toString()str=str.replace(/异步\s+(函数)\s+/,'$1*').replace(/await/g,'yield')返回str}