详解ES6之async+await 同步/异步方案
异步编程一直是JavaScript编程的重大事项。关于异步方案,ES6先是出现了基于状态管理的Promise,然后出现了Generator函数+co函数,紧接着又出现了ES7的async+await方案。
本文力求以最简明的方式来疏通async+await。
异步编程的几个场景
先从一个常见问题开始:一个for循环中,如何异步的打印迭代顺序?
我们很容易想到用闭包,或者ES6规定的let块级作用域来回答这个问题。
for(letvalof[1,2,3,4]){ setTimeout(()=>console.log(val),100); } //=>预期结果依次为:1,2,3,4
这里描述的是一个均匀发生的的异步,它们被依次按既定的顺序排在异步队列中等待执行。
如果异步不是均匀发生的,那么它们被注册在异步队列中的顺序就是乱序的。
for(letvalof[1,2,3,4]){ setTimeout(()=>console.log(val),100*Math.random()); } //=>实际结果是随机的,依次为:4,2,3,1
返回的结果是乱序不可控的,这本来就是最为真实的异步。但另一种情况是,在循环中,如果希望前一个异步执行完毕、后一个异步再执行,该怎么办?
for(letvalof['a','b','c','d']){ //a执行完后,进入下一个循环 //执行b,依此类推 }
这不就是多个异步“串行”吗!
在回调callback嵌套异步操作、再回调的方式,不就解决了这个问题!或者,使用Promise+then()层层嵌套同样也能解决问题。但是,如果硬是要将这种嵌套的方式写在循环中,还恐怕还需费一番周折。试问,有更好的办法吗?
异步同步化方案
试想,如果要去将一批数据发送到服务器,只有前一批发送成功(即服务器返回成功的响应),才开始下一批数据的发送,否则终止发送。这就是一个典型的“for循环中存在相互依赖的异步操作”的例子。
明显,这种“串行”的异步,实质上可以当成同步。它和乱序的异步比较起来,花费了更多的时间。按理说,我们希望程序异步执行,就是为了“跳过”阻塞,较少时间花销。但与之相反的是,如果需要一系列的异步“串行”,我们应该怎样很好的进行编程?
对于这个“串行”异步,有了ES6就非常容易的解决了这个问题。
asyncfunctiontask(){ for(letvalof[1,2,3,4]){ //await是要等待响应的 letresult=awaitsend(val); if(!result){ break; } } } task();
从字面上看,就是本次循环,等有了结果,再进行下一次循环。因此,循环每执行一次就会被暂停(“卡住”)一次,直到循环结束。这种编码实现,很好的消除了层层嵌套的“回调地狱”问题,降低了认知难度。
这就是异步问题同步化的方案。关于这个方案,如果说Promise主要解决的是异步回调问题,那么async+await主要解决的就是将异步问题同步化,降低异步编程的认知负担。
async+await“外异内同”
早先接触这套API时,看着繁琐的文档,一知半解的认为async+await主要用来解决异步问题同步化的。
其实不然。从上面的例子看到:async关键字声明了一个异步函数,这个异步函数体内有一行await语句,它告示了该行为同步执行,并且与上下相邻的代码是依次逐行执行的。
将这个形式化的东西再翻译一下,就是:
1、async函数执行后,总是返回了一个promise对象
2、await所在的那一行语句是同步的
其中,1说明了从外部看,task方法执行后返回一个Promise对象,正因为它返回的是Promise,所以可以理解task是一个异步方法。毫无疑问它是这样用的:
task().then((val)=>{alert(val)}) .then((val)=>{alert(val)})
2说明了在task函数内部,异步已经被“削”成了同步。整个就是一个执行稍微耗时的函数而已。
综合1、2,从形式上看,就是“task整体是一个异步函数,内部整个是同步的”,简称“外异内同”。
整体是一个异步函数不难理解。在实现上,我们不妨逆向一下,语言层面让async关键字调用时,在函数执行的末尾强制增加一个promise反回:
asyncfn(){ letresult; //... //末尾返回promise returnisPromise(result)? result:Promise.resolve(undefined); }
内部是同步的是怎么做到的?实际上await调用,是让后边的语句(函数)做了一个递归执行,直到获取到结果并使其状态变更,才会resolve掉,而只有resolve掉,await那一行代码才算执行完,才继续往下一行执行。所以,尽管外部是一个大大的for循环,但是整个for循环是依次串行的。
因此,仅从上述框架的外观出发,就不难理解async+await的意义。使用起来也就这么简单,反而Promise是一个必须掌握的基础件。
秉承本次《重读ES6》系列的原则,不过多追求理解细节和具体实现过程。我们继续巩固一下这个“形式化”的理解。
async+await的进一步理解
有这样的一个异步操作longTimeTask,已经用Promise进行了包装。借助该函数进行一系列验证。
constlongTimeTask=function(time){ returnnewPromise((resolve,reject)=>{ setTimeout(()=>{ console.log(`等了${time||'xx'}年,终于回信了`); resolve({'msg':'taskdone'}); },time||1000) }) }
async函数的执行情况
如果,想查看asyncexec1函数的返回结果,以及await命令的执行结果:
constexec1=asyncfunction(){ letresult=awaitlongTimeTask(); console.log('resultafterlongtime===>',result); } //查看函数内部执行顺序 exec1(); //=>等了xx年,终于回信了 //=>resultafterlongtime===>Object{msg:"taskdone"} //查看函数总体返回值 console.log(exec1()); //=>Promise{[[PromiseStatus]]:"pending",...} //=>同上
以上2步执行,清晰的证明了exec1函数体内是同步、逐行逐行执行的,即先执行完异步操作,然后进行console.log()打印。而exec1()的执行结果就直接是一个Promise,因为它最先会蹦出来一串Promise...,然后才是exec1函数的内部执行日志。
因此,所有验证,完全符合整体是一个异步函数,内部整个是同步的的总结。
await如何执行其后语句?
回到await,看看它是如何执行其后边的语句的。假设:让longTimeTask()后边直接带then()回调,分两种情况:
1)then()中不再返回任何东西
2)then()中继续手动返回另一个promise
constexec2=asyncfunction(){ letresult=awaitlongTimeTask().then((res)=>{ console.log('then===>',res.msg); res.msg=`${res.msg}thenrefrashmessage`; //注释掉这条return或手动返回一个promise returnPromise.resolve(res); }); console.log('resultafterawait===>',result.msg); } exec2(); //=>情况一TypeError:Cannotreadproperty'msg'ofundefined //=>情况二正常
首先,longTimeTask()加上再多得then()回调,也不过是放在了它的回调列队queue里了。也就是说,await命令之后始终是一条表达式语句,只不过上述代码书写方式比较让人迷惑。(比较好的实践建议是,将longTimeTask方法身后的then()移入longTimeTask函数体封装起来)
其次,手动返回另一个promise和什么也不返回,关系到longTimeTask()方法最终resolve出去的内容不一样。换句话说,await命令会提取其后边的promise的resolve结果,进而直接导致result的不同。
值得强调的是,await命令只认resolve结果,对reject结果报错。不妨用以下的return语句替换上述return进行验证。
returnPromise.reject(res);
最后
其实,关于异步编程还有很多可以梳理的,比如跨模块的异步编程、异步的单元测试、异步的错误处理以及什么是好的实践。Allinall,限于篇幅,不在此汇总了。最后,async+await确实是一个很优雅的方案。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。