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

Async:一种简洁优雅的异步方式

时间:2023-03-12 23:55:12 科技观察

前言在异步处理方案中,最简洁优雅的就是async函数(以下简称A函数)。经过必要的block封装后,A函数可以像同步操作一样聚合多个相关的异步操作,使得它们之间的关系更清晰,流程更简洁,调试更方便。它本质上是Generator函数的语法糖。通俗地说,就是使用G函数进行异步处理的加强版。如果你尝试去学习A函数,你必须要有Promise基础,你还必须了解Generator函数。如果需要,可以查看扩展部分。为了直观感受A函数的魅力,下面使用Promise和A函数进行同样的异步操作。异步的目的是获取用户的消息列表,需要分页,由后台控制。具体操作是:首先获取消息总数,然后修正当前要显示的页数(每次切换到不同的页面,总数可能会发生变化),最后传递参数并获取相应的数据.lettotalNum=0;//Totalcommentsnumber.letcurPage=1;//Currentpageindex.letpageSize=10;//Thenumberofcommentdisplayedinonepage.//主要代码使用A函数。asyncfunctiondealWithAsync(){totalNum=awaitgetListCount();console.log('Getcount',totalNum);if(pageSize*(curPage-1)>totalNum){curPage=1;}returngetListData();}//使用Promise主代码.functiondealWithPromise(){returnnewPromise((resolve,reject)=>{getListCount().then(res=>{totalNum=res;console.log('Getcount',res);if(pageSize*(curPage-1)>totalNum){curPage=1;}returngetListData()}).then(resolve).catch(reject);});}//开始执行dealWithAsync函数。//dealWithAsync().then(res=>{//console.log('GetData',res)//}).catch(err=>{//console.log(err);//});//开始执行dealWithPromise函数。//dealWithPromise().then(res=>{//console.log('GetData',res)//}).catch(err=>{//console.log(err);//});functiongetListCount(){returncreatePromise(100).catch(()=>{throw'Getlistcounterror';});}functiongetListData(){returncreatePromise([],{curPage:curPage,pageSize:pageSize,}).catch(()=>{throw'Getlistdataerror';});}functioncreatePromise(data,//Rebackdataparams=null,//RequestparamsisSucceed=true,timeout=1000,){returnnewPromise((resolve,reject)=>{setTimeout(()=>{isSucceed?resolve(data):reject(data);},timeout);});}对比dealWithAsync和dealWithPromise这两个简单的函数,可以直观的发现,使用A函数,除了await关键字,类似于同步代码没有区别。但是使用Promise需要按照规则添加很多包裹链操作,导致回调函数过多,不够简单。另外,这里将各个异步操作分开,并指定了每次成功或失败时传输的数据,更贴近实际开发。1启示1.1形式函数也是函数,所以具有普通函数应有的性质。但是在形式上有两点不同:首先,在定义A函数的时候,在function关键字之前需要加上async关键字(异步的意思),表示这是一个A函数。二是在A函数内部可以使用await关键字(意思是等待),意思是后面的结果会被当作异步操作,等待它完成。这里有几种定义它的方法。//declarativeasyncfunctionA(){}//表达式letA=asyncfunction(){};//作为对象属性leto={A:asyncfunction(){}};//作为对象属性的简写leto={asyncA(){}};//箭头函数leto={A:async()=>{}};1.2返回值执行函数A,固定返回一个Promise对象。获取到对象后,可以监听设置成功或失败时的回调函数。如果函数执行成功并结束,则返回的P对象的状态将从waiting变为successful,并输出return命令的返回结果(否则为undefined)。如果函数在执行过程中失败,JS会认为A函数已经执行完毕,返回的P对象的状态会由waiting变为failure,并输出错误信息。//成功执行caseA1().then(res=>{console.log('执行成功',res);//10});asyncfunctionA1(){letn=1*10;returnn;}//执行失败CaseA2().catch(err=>{console.log('执行失败',err);//iisnotdefined.});asyncfunctionA2(){letn=1*i;returnn;}1.3await只存在于A函数内部只能使用await命令,存在于A函数内部的普通函数不能。引擎会统一把await之后后面的值当做一个Promise,调用Promise.resolve()将不是Promise对象的值进行转换。即使这个值是一个Error实例,在转换之后,引擎仍然认为它是一个成功的Promise,它的数据是一个Error实例。当函数执行到await命令时,会暂停执行,等待后续的Promise结束。如果P对象最后成功了,它会返回一个成功的返回值,相当于把awaitxxx换成了返回值。如果P对象最后失败了,错误没有被捕捉到,引擎会直接停止执行A函数,并将返回对象的状态变为失败,并输出错误信息。***,A函数中的returnx表达式相当于returnawaitx的缩写。//成功执行caseA1().then(res=>{console.log('执行成功',res);//大约两秒后输出100。});asyncfunctionA1(){letn1=await10;letn2=awaitnewPromise(resolve=>{setTimeout(()=>{resolve(10);},2000);});returnn1*n2;}//执行失败案例A2().catch(err=>{console.log('执行失败',err);//大约两秒后输出10。});asyncfunctionA2(){letn1=await10;letn2=awaitnewPromise((resolve,reject)=>{setTimeout(()=>{reject(10);},2000);});returnn1*n2;}2入侵2.1子序列和并发对于JS语句中存在的await命令(for,while等),引擎也会在遇到时暂停执行。这意味着可以使用循环语句直接处理多个异步。下面是两个处理次级的例子。A函数在处理连续异步的时候特别简洁,整体上和同步代码没什么区别。//A1和A2这两个方法的行为结果是一样的,都是每秒输出10,输出3次。asyncfunctionA1(){letn1=awaitcreatePromise();console.log('N1',n1);letn2=awaitcreatePromise();console.log('N2',n2);letn3=awaitcreatePromise();console.log('N3',n3);}asyncfunctionA2(){for(leti=0;i<3;i++){letn=awaitcreatePromise();console.log('N'+(i+1),n);}}functioncreatePromise(){returnnewPromise(resolve=>{setTimeout(()=>{resolve(10);},1000);});}下面是三个处理并发的例子。A1函数使用Promise.all异步生成聚合。虽然简单,但是灵活性降低了。只有成功和失败两种情况。A3函数相对于A2只是为了说明如何使用数组遍历方法使用async函数。重点是对A2函数的理解。A2函数使用循环语句,其实是二次获取每一个异步值,但是在整体时间上是相当并发的(这个需要好好理解)。因为在一开始创建reqs数组的时候,每一个异步都已经执行过了,虽然是一个一个的获取,但是总的耗时与遍历顺序无关,总是等于异步最耗时(不考虑遍历、执行等耗时)。//A1、A2、A3这三个方法的行为结果是一样的,都在大约一秒后输出[10,10,10]。asyncfunctionA1(){letres=awaitPromise.all([createPromise(),createPromise(),createPromise()]);console.log('Data',res);}asyncfunctionA2(){letres=[];letreqs=[createPromise(),createPromise(),createPromise()];for(leti=0;i{letn=awaitcreatePromise(item);returnn+1;});for(leti=0;i{setTimeout(()=>{resolve(n);},1000);});}2.2错误处理一旦await背后的Promise变为rejected,整个async函数就会终止。但是,很多时候我们并不想因为一个异步操作失败而终止整个函数,所以需要进行合理的错误处理。注意,这里所说的错误不包括引擎解析错误或执行错误,只是状态变为rejected的Promise对象。有两种处理方式:一种是提前包装Promise对象,让它一直返回一个成功的Promise。第二种是使用try.catch来捕获错误。//A1和A2都执行了,返回值为10A1().then(console.log);A2().then(console.log);asyncfunctionA1(){letn;n=awaitcreatePromise(true);returnn;}asyncfunctionA2(){letn;try{n=awaitcreatePromise(false);}catch(e){n=e;}returnn;}functioncreatePromise(needCatch){letp=newPromise((resolve,reject)=>{reject(10);});returnneedCatch?p.catch(err=>err):p;}2.3实现原理前言中提到,A函数是使用G函数进行异步处理的增强版。既然如此,那我们就从它的改进方面入手,看看它基于G函数的实现原理。A函数相对于G函数的改进体现在这些方面:更好的语义、内置的执行器和返回值是Promise。更好的语义。G函数是在函数后面用*号标示为G函数,A函数是在函数前加上async关键字。在G函数中可以使用yield命令暂停执行,交出执行权,而A函数中使用await等待异步返回结果。显然,async和await更具语义。//G函数function*request(){letn=yieldcreatePromise();}//A函数asyncfunctionrequest(){letn=awaitcreatePromise();}functioncreatePromise(){returnnewPromise(resolve=>{setTimeout(()=>{resolve(10);},1000);});}内置执行器。调用A函数会自动执行,一步步等待异步操作,直到结束。如果需要使用G函数自动执行异步操作,需要为其创建一个自执行器。G函数的执行是通过自执行器自动执行的,其行为与A函数基本相同。可以说A功能相对于G功能最大的改进就是内置了自执行器。//都每秒打印10,重复两次。//A函数A();asyncfunctionA(){letn1=awaitcreatePromise();console.log(n1);letn2=awaitcreatePromise();console.log(n2);}//G函数,使用self-executor来执行。spawn(G);function*G(){letn1=yieldcreatePromise();console.log(n1);letn2=yieldcreatePromise();console.log(n2);}functionspawn(genF){returnnewPromise(函数(解决,拒绝){constgen=genF();functionstep(nextF){letnext;try{next=nextF();}catch(e){returnreject(e);}if(next.done){returnresolve(next.value);}Promise.resolve(next.value).then(function(v){step(function(){returgen.next(v);});},function(e){step(function(){returgen.throw(e);});});}step(function(){returgengen.next(undefined);});});}functioncreatePromise(){returnnewPromise(resolve=>{setTimeout(()=>{resolve(10);},1000);});}2.4执行顺序在了解函数A内部和外部包括它的执行顺序之前,需要先了解两点:一是Promise的实例方法是延迟到本轮事件结束执行操作,详见链接。二是Generator函数通过调用实例方法来切换执行权,控制程序执行顺序。有关详细信息,请参阅链接。了解了A函数的执行顺序,就可以更清晰的掌握这三者的存在。先看下面的代码,比较方法A1、A2、A3的结果。F(A1);//依次打印出来:13425.F(A2);//依次打印出:13245。女(A3);//首先打印出:132,两秒后打印出:49.functionF(A){console.log(1);A().then(console.log);console.log(2);}asyncfunctionA1(){console.log(3);console.log(4);return5;}asyncfunctionA2(){console.log(3);letn=await5;console.log(4);returnn;}asyncfunctionA3(){console.log(3);letn=awaitcreatePromise();console.log(4);returnn;}functioncreatePromise(){returnnewPromise(resolve=>{setTimeout(()=>{resolve(9);},2000);});}从结果中可以总结出一些表面形式。当函数A执行时,它的函数体会立即执行,直到遇到await命令。遇到await命令后,执行权会转移到A函数外部,即不管A函数内部是否执行,都会执行外部代码。外部代码(当前事件)执行完后,继续执行之前await命令后面的代码。总结到此已经成功了一半,接下来着手分析其原因。客官,你要是对这栋楼有所了解,应该不会忘记了‘自动执行人’的阿姨吧?我想你忘了。A函数的本质是带有自执行器的G函数,所以探索A函数的执行原理就是探索使用自执行器的G函数的执行原理。记住?再看下面的代码,使用相同逻辑的G函数会得到与A函数相同的结果。F(A);//先打印出:132,两秒后打印出:49.F(()=>{returnspawn(G);});//先打印出:132,再打印出:49两秒后。functionF(A){console.log(1);A().then(console.log);console.log(2);}asyncfunctionA(){console.log(3);letn=awaitcreatePromise();控制台。log(4);returnn;}function*G(){console.log(3);letn=yieldcreatePromise();console.log(4);returnn;}functioncreatePromise(){returnnewPromise(resolve=>{setTimeout(()=>{resolve(9);},2000);});}functionspawn(genF){returnnewPromise(function(resolve,reject){constgen=genF();functionstep(nextF){letnext;try{next=nextF)();}catch(e){returnreject(e);}if(next.done){returnresolve(next.value);}Promise.resolve(next.value).then(function(v){step(function(){returngen.next(v);});},function(e){step(function(){returngen.throw(e);});});}step(function(){returngen.next(undefined);});});}G函数自动执行时,遇到yield命令后会使用Promise.resolve对表达式进行包装,并为其设置回调函数。不管Promise是立即有结果还是一定时间后有结果,它的回调函数都会延迟到本轮事件结束。然后下一步,再下一步。A函数也是同理,遇到await命令(这里省略三五个字符),所以有这样的执行顺序。谢幕。