详解async/await 异步应用的常用场景
前言
async/await语法用看起来像写同步代码的方式来优雅地处理异步操作,但是我们也要明白一点,异步操作本来带有复杂性,像写同步代码的方式并不能降低本质上的复杂性,所以在处理上我们要更加谨慎,稍有不慎就可能写出不是预期执行的代码,从而影响执行效率。下面将简单地描述一下一些日常常用场景,加深对async/await认识
最普遍的异步操作就是请求,我们也可以用setTimeOut来简单模拟异步请求。
场景1.一个请求接着一个请求
相信这个场景是最常遇到,后一个请求依赖前一个请求,下面以爬取一个网页内的图片为例子进行描述,使用了superagent请求模块,cheerio页面分析模块,图片的地址需要分析网页内容得出,所以必须按顺序进行请求。
constrequest=require('superagent') constcheerio=require('cheerio') //简单封装下请求,其他的类似 functiongetHTML(url){ //一些操作,比如设置一下请求头信息 returnsuperagent.get(url).set('referer',referer).set('user-agent',userAgent) } //下面就请求一张图片 asyncfunctionimageCrawler(url){ letres=awaitgetHTML(url) lethtml=res.text let$=cheerio.load(html) let$img=$(selector)[0] lethref=$img.attribs.src res=awaitgetImage(href) retrunres.body } asyncfunctionhandler(url){ letimg=awaitimageCrawler(url) console.log(img)//buffer格式的数据 //处理图片 } handler(url)
上面就是一个简单的获取图片数据的场景,图片数据是加载进内存中,如果只是简单的存储数据,可以用流的形式进行存储,以防止消耗太多内存。
其中awaitgetHTML是必须的,如果省略了await程序就不能按预期得到结果。执行流程会先执行await后面的表达式,其实际返回的是一个处于pending状态的promise,等到这个promise处于已决议状态后才会执行await后面的操作,其中的代码执行会跳出async函数,继续执行函数外面的其他代码,所以并不会阻塞后续代码的执行。
场景2.并发请求
有的时候我们并不需要等待一个请求回来才发出另一个请求,这样效率是很低的,所以这个时候就需要并发执行请求任务。下面以一个查询为例,先获取一个人的学校地址和家庭住址,再由这些信息获取详细的个人信息,学校地址和家庭住址是没有依赖关系的,后面的获取个人信息依赖于两者
asyncfunctioninfoCrawler(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()连async都省了 functionsaveImages(imageUrls){ imageUrls.forEach(asyncimageUrl=>{ letimg=awaitgetImage(imageUrl) saveImage(img) }) }
可能有人会疑问forEach不是不能用于异步吗,这个说法我也在刚接触这个语法的时候就听说过,很明显forEach是可以处理异步的,只是是并发处理,map也是并发处理,这个怎么用主要看你的实际场景,还要看你是否对结果感兴趣
场景3.错误处理
一个请求发出,可以会遇到各种问题,我们是无法保证一定成功的,报错是常有的事,所以处理错误有时很有必要,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) })
可能有人会有疑问,是不是要在每个请求中都try/catch一下,这个其实你在最外层catch一下就可以了,一些基于中间件的设计就喜欢在最外层捕获错误
asyncfunctionctx(next){ try{ awaitnext() }catch(error){ console.log(error) } }
场景4.超时处理
一个请求发出,我们是无法确定什么时候返回的,也总不能一直傻傻的等,设置超时处理有时是很有必要的
functiontimeOut(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的状态一经改变是不会再改变的
场景5.并发限制
在并发请求的场景中,如果需要大量并发,必须要进行并发限制,不然会被网站屏蔽或者造成进程崩溃
asyncfunctiongetImages(urls,limit){ letrunning=0 letr letp=newPromise((resolve,reject)=>{ r=resolve }) functionrun(){ if(running0){ running++ leturl=urls.shift(); (async()=>{ letimg=awaitgetImage(url) running-- console.log(img) if(urls.length===0&&running===0){ console.log('done') returnr('done') }else{ run() } })() run()//立即到并发上限 } } run() returnawaitp }
总结
以上列举了一些日常场景处理的代码片段,在遇到比较复杂场景时,可以结合以上的场景进行组合使用,如果场景过于复杂,最好的办法是使用相关的异步代码控制库。如果想更好地了解async/await可以先去了解promise和generator,async/await基本上是generator函数的语法糖,下面简单的描述了一下内部的原理。
functiondelay(time){ returnnewPromise((resolve,reject)=>{ setTimeout(()=>{ resolve(time) },time) }) } function*createTime(){ lettime1=yielddelay(1000) lettime2=yielddelay(2000) lettime3=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就可以设置生成器内部的值,这里是手动传入,我只要写一个递归函数让其自动添进去就可以了
functionrun(createTime){ letiterator=createTime() letresult=iterator.next() functionautoRun(){ if(!result.done){ Promise.resolve(result.value).then(time=>{ result=iterator.next(time) autoRun() }).catch(err=>{ result=iterator.throw(err) autoRun() }) } } autoRun() } run(createTime)
promise.resove保证返回的是一个promise对象可迭代对象除了有next方法还有throw方法用于往生成器内部传入错误,只要生成内部能捕获该对象,生成器就可以继承运行,类似下面的代码
functiondelay(time){ returnnewPromise((resolve,reject)=>{ setTimeout(()=>{ if(time==2000){ reject('2000错误') } resolve(time) },time) }) } function*createTime(){ lettime1=yielddelay(1000) lettime2 try{ time2=yielddelay(2000) }catch(error){ time2=error } lettime3=yielddelay(3000) console.log(time1,time2,time3) }
可以看出生成器函数其实和async/await语法长得很像,只要改一下async/await代码片段就是生成器函数了
asyncfunctioncreateTime(){ lettime1=awaitdelay(1000) lettime2 try{ time2=awaitdelay(2000) }catch(error){ time2=error } lettime3=awaitdelay(3000) console.log(time1,time2,time3) } functiontransform(async){ letstr=async.toString() str=str.replace(/async\s+(function)\s+/,'$1*').replace(/await/g,'yield') returnstr }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。