当前位置: 首页 > 科技观察

JavaScript异步编程指南—Async-Await解决方案

时间:2023-03-12 00:06:10 科技观察

本文转载自微信公众号《五月君》,作者吴跃君。转载本文请联系MayJun公众号。ES7之后引入了Async/Await来解决异步编程,目前被称为JavaScript异步编程中的“终极解决方案”。基本使用函数声明时,在function关键字前使用async关键字,内部使用await代替Generator中的yield,语义上比Generator中的*更清晰。与Generator相比,Async/Await内置了执行器,不需要co.等外部模块。编程语言本身的实现是最好的,也更容易使用。声明async函数下面是基于Generator中的一个例子进行的修改。第二次await后,用Promise封装。它本身支持一个Promise对象。此时会等待Promise状态变为Fulfilled后执行下一步。当Promise无法正常执行resolve/reject时,意味着后面的不会执行。await后面也可以跟基本类型:数值、字符串、布尔值,不过这时候它也会立马变成一个Fulfilled状态的Promise。asyncfunctiontest(){constres1=await'A';constres2=awaitPromise.resolve(res1+'B');constres3=awaitres2+'C';returnres3;}(async()=>{constres=awaittest();//ABC})();错误管理如果await背后的Promise返回错误,则需要try/catch来捕获错误。如果有多个await操作,可以全部放在一起。这样的话,如果第一个await之后的Promise报错,第二个await就不会执行了。这和正常的函数操作基本一样,不同的是对于异步函数我们需要加上await关键字。(async()=>{try{awaitfetch1(url);awaitfetch2(url);}catch(err){//TODO}})();另外注意async函数中一定要写await,否则会报SyntaxError:awaitisonlyvalidinasyncfunctionsandthetoplevelbodyofmodules。//错误操作(()=>{await'A';})();这样写是不行的,在“协程”讲义中也提到过类似的例子,不过当时是基于yield表达式,async/await其实是Generator函数的一个语法糖,内部机制同理,forEach中的匿名函数是一个普通函数,在运行时会被当做一个子函数,而栈协程是从子函数函数中来的,ES6中实现的协程是stackless协程,只能从生成器内部生成。以下代码在运行时完全失败。(async()=>{['B','C'].forEach(item=>{constres=awaititem;console.log(res);})})();如果想通过await表达式正常运行,只是为了避免使用回调函数,可以使用迭代器for...of。(async()=>{for(constitemof['B','C']){constres=awaititem;//BC}})();并发执行当我们有多个异步请求并且不必顺序执行时,最好在await表达式之后使用Promise.all()。(async()=>{awaitPromise.all([fetch(url1),fetch(ur2)])})();从这个例子我们可以看出async/await也是基于Promise的。异步迭代上面解释的Async/Await的使用是基于单次运行的异步函数。在Node.js中,我们还有一类需求来自持续事件触发。例如,基于流式API读取数据很常见。注册on('data',callback)事件和回调函数,但这样我们就不能使用常规的Async/Await表达式来处理此类场景。异步迭代器异步迭代器和同步迭代器的区别在于,可迭代的异步迭代器对象具有[Symbol.asyncIterator]属性,并返回Promise.resolve({value,done})结果。实现异步迭代器的更方便的方法是使用声明为async的生成器函数,它允许我们像使用常规函数一样使用await。下面展示了Node.js可读流对象是如何实现异步可迭代的。出了核心代码,作者还有一篇关于异步迭代器的详细文章,非常精彩。如果您有兴趣,可以阅读并探索Node.js中异步迭代器的使用。//forawait...of循环会调用Readable.prototype[SymbolAsyncIterator]=function(){...constiter=createAsyncIterator(stream);returniter;};//声明一个生成器函数asyncfunction*,创建一个异步迭代器对象createAsyncIterator(stream){...try{while(true){//stream.read()从内部缓冲区中提取并返回数据。如果没有可读数据,调用readable的destroy()方法后返回null//readable.destroyed为true,可读为如下流对象constchunk=stream.destroyed?null:stream.read();if(chunk!==null){yieldchunk;//这里是关键。根据迭代器协议的定义,迭代器对象应该返回一个next()方法,使用yield返回每次的值}...}}catch(err){}}for...await...ofTraverserNode.jsStream模块的可读流对象在v10.0.0版本中实验性支持[Symbol.asyncIterator]属性,可以使用forawait...of语句遍历可读流对象已经被支持v11.14.0及以上版本的LTS,这使得我们从流中读取连续的数据块非常方便。constfs=require('fs');constreadable=fs.createReadStream('./hello.txt',{encoding:'utf-8'});asyncfunctionreadText(readable){letdata='';forawait(constchunkofreadable){data+=chunk;}returndata;}(async()=>{try{constres=awaitreadText(readable);console.log(res);//HelloNode.js}catch(err){console.log(err.message);}})();使用**forawait...of**语句遍历可读。如果循环由于中断或抛出错误而终止,则Stream也将被销毁。顶层Await根据async函数的语法规则,await只能出现在async异步函数中。对于异步资源,我们之前不得不在async函数中使用await,这对于一些需要在文件顶部实例化的资源可能效果不佳。Node.jsv14.xLTS发布后,已经支持了顶层的Await,我们可以很方便的在文件顶部初始化这些异步资源。我们可以像下面这样写,但是这种模式也只有在ESModules中才有。importfetchfrom'node-fetch';constres=awaitfetch(url)总结JavaScript编程中的大部分操作都是异步编程。Async/Await可以用同步的方式来写我们的代码,但是实际的执行还是异步的。该方法目前也被称为异步编程的终极解决方案。