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

异步编程的终极解决方案 async-await:用同步的方式去写异步代码

时间:2023-03-27 12:11:57 JavaScript

异步编程async/await的终极解决方案:用同步的方式写异步代码;})但是这个回调函数有一个很大的缺陷,就是会写回调地狱(Callbackhell)。比如多个回调有依赖关系,可以这样写:ajax(url,(res)=>{console.log(res);//...处理代码ajax(url2,(res2)=>{console.log(res2);//...处理代码ajax(url3,(res3)=>{console.log(res3);//...处理代码})})})这是回调地狱:嵌入式函数存在Coupling,一招牵一发而动全身,改一个又会影响其他地方的内嵌函数,出错怎么处理?这是一个难题。早期回调函数的优缺点:优点:解决了同步阻塞的问题(只要一个任务耗时长,后面的任务就必须排队,会延迟整个程序的执行)缺点:回调地狱;不能用trycatch来捕捉错误;can'treturntransitionschemeGeneratorES6新引入了Generator函数(generatorfunction),可以通过yield关键字暂停函数的执行流程,提供了改变执行流程的可能,从而提供了异步编程的解决方案。最大的特点是可以控制函数的执行。Generator有两部分区别于普通函数:一是在函数后面,函数名前有一个*,用来表示该函数是一个Generator函数。函数内部有一个yield表达式,用来定义函数内部的状态。Generator函数的具体使用方法是:在Generator函数内部执行一段代码,如果遇到yield关键字,JS引擎会将关键字后面的内容返回给外部,并暂停该函数的执行。外部函数可以通过next方法恢复函数执行。function*fn(){console.log("one");yield'1';console.log("two");yield'2';console.log("three");return'3';}调用Generator函数和调用普通函数一样,只是在函数名后面加上(),但是Generator函数不会像普通函数一样立即执行,而是返回一个指向内部状态对象的指针,所以需要调用迭代器对象Iteratornext方法,指针将从函数头或上次停止的地方开始执行。如下:next方法:一般情况下,当next方法不传入参数时,yield表达式的返回值是undefined。next传入参数时,该参数将作为上一步yield的返回值。Generator生成器也是用同步的方式写异步代码,这样也可以解决回调地狱的问题,但是比较难理解。希望下面的例子可以帮助大家理解Generator生成器:function*sum(a){console.log('a:',a);让b=产量1;console.log('b:',b);让c=产量2;console.log('c:',c);让总和=a+b+c;console.log('sum:',sum)returnsum;}next没有传递参数时,yield返回undefined第一次执行next时,传入的参数会被忽略,函数暂停在yield1处,所以return1第二次执行next时,没有传参数,然后yield1返回undefined,所以b的值为undefined。同样,c的值第三次未定义。当next作为参数传入时,该参数将作为上面单步yield的返回值,如下图所示:第一次执行next时,参数(20)会被忽略,函数在yield1处暂停,所以返回1。第二次执行next时,传入参数30作为yield1返回的值,所以b=yield1,b的值为30next时第二次执行时,参数40作为yield2的返回值传入,所以c=yield2,c的值为40coroutine我们知道,async/await是一个自动执行的Generator函数。上面介绍了Generator函数,那么有必要介绍一下V8引擎是如何实现一个函数的暂停和恢复的。要理解函数为什么可以挂起和恢复,首先要理解协程的概念。我们都知道进程和线程,那么什么是协程呢?协程比线程更轻量级。协程可以看作是运行在线程上的任务。一个线程上可以有多个协程,但一个线程上同时只能执行一个协程。比如当前正在执行协程A,需要启动协程B,则A协程需要将主线程的控制权交给B协程,体现在A协程暂停执行,B协程恢复执行;同样,A协程也可以从B协程启动。通常,如果协程B是从协程A启动的,我们称协程A为协程B的父协程。正如一个进程可以有多个线程一样,一个线程也可以有多个协程。最重要的是,协程不由操作系统内核管理,而完全由程序控制(即在用户态执行)。这样做的好处是性能有了很大的提升,不会像线程切换那样消耗资源。结合代码可以理解:function*genDemo(){console.log("开始执行第一段")yield'generator2'console.log("开始执行第二段")yield'generator2'console.log("开始执行第三段")yield'generator2'console.log("执行结束")return'generator2'}console.log('main0')letgen=genDemo()console.log(gen.next().value)console.log('main1')console.log(gen.next().value)console.log('main2')console.log(gen.next().value)console.log('main3')console.log(gen.next().value)console.log('main4')执行过程如下图所示,可以重点关注协程之间的切换:从图中可以看出程序的四点规则:通过调用生成器函数genDemo创建一个协程gen。创建后,gen协程不会立即执行。要让gen协程执行,需要调用gen.next。协程执行时,可以使用yield关键字暂停gen协程的执行,将主要信息返回给父协程。如果协程在执行过程中遇到return关键字,JS引擎会结束当前协程,将return后的内容返回给父协程。协程切换:gen协程和父协程在主线程上交互执行,不是并发执行。他们之前的切换是通过yield和gen.next的配合完成的。当在gen协程中调用yield方法时,JS引擎会保存gen协程当前的调用栈信息,并恢复父协程的调用栈信息。同样,在父协程中执行gen.next时,JS引擎会保存父协程的调用栈信息,并恢复gen协程的调用栈信息。其实在JS中,Generator生成器就是协程的一种实现。async/await的最终解决方案是使用Promise来解决回调地狱的问题,但是这个方法充满了Promise的then()方法。如果处理流程比较复杂,那么整个代码就会充满then,语义也不好。显然,代码并不能很好地表示执行流程。为此,ES7引入了async/await,这是对JavaScript异步编程的重大改进,提供了使用同步代码异步访问资源而不阻塞主线程的能力,使代码逻辑更加清晰。其实async/await技术背后的秘密就是Promise和Generator的应用,更底层就是microtasks和coroutines的应用。要理解async和await是如何工作的,我们必须分别分析async和await。什么是异步?根据MDN定义,async是一个异步执行并隐式返回Promise作为结果的函数。关注两个词:异步执行和隐式返回一个Promise。先来看看如何隐式返回Promise,参考如下代码:asyncfunctionasync1(){return'秀儿';}console.log(async1());//Promise{:"秀儿"}执行这段代码,可以看到调用async声明的async1函数返回一个Promise对象,状态为resolved,返回结果如下:Promise{:《秀儿》}。它与处理Promise链调用中的返回值是一样的。awaitawait需要配合async使用。结合以下代码看看await是什么:asyncfunctionfoo(){console.log(1)leta=await100console.log(a)console.log(2)}console.log(0)foo()console.log(3)从协程的角度来看这段代码的整体执行流程图:结合上图分析async/await的执行流程:首先,执行控制台。log(0)语句打印出0。下一步是执行foo函数。由于foo函数被标记为async,所以在进入该函数时,JS引擎会保存当前的调用栈等信息,然后在foo函数中执行console.log(1)语句。并打印出1.当执行到await100时,会默认创建一个Promise对象。代码如下:letpromise_=newPromise((resolve,reject){resolve(100)})在创建这个promise_对象的过程中,可以看到在executor函数中调用了resolve函数,并且JS引擎将任务提交到微任务队列。然后JS引擎会暂停当前协程的执行,将主线程的控制权交给父协程执行,并将promise_对象返回给父协程。主线程的控制权已经交给了父协程。这时候父协程要做的一件事就是调用promise_.then来监听promise状态的变化。接下来继续执行父协程的流程,执行console.log(3),打印出3。那么父协程的执行就结束了。结束前会进入microtask的checkpoint,然后执行microtask队列。微任务队列中有resolve(100)个任务等待执行。当执行到这里时,promise_.then中的回调函数如下:promise_.then((value)=>{//激活回调函数后,//将主线程的控制权交给foo协程,将vaule值传递给协程})回调函数启动后,会将主线程的控制权交给foo函数的协程,同时将value值传递给协程。foo协程启动后,会将之前的值赋给变量a,然后foo协程会继续执行后面的语句。执行完成后,控制权会交还给父协程。以上就是await/async的执行流程。正是因为async和await在幕后做了很多工作,我们才可以用同步的方式来写异步代码。当然也有一些缺点,因为await将异步代码转化成了同步代码。如果多个异步代码没有依赖关系而使用await,性能会降低。async/await总结Promise的编程模型仍然充斥着大量的then方法。虽然解决了回调地狱的问题,但是在语义上仍然存在缺陷。代码中充斥着大量的then函数,这也是async/await出现的原因。Async/await可用于以同步代码的风格编写异步代码。这是因为async/await的基础技术使用了Generator生成器和Promise。Generator生成器是协程的实现,Generator生成器可以用来实现生成器。暂停和恢复功能。另外,V8引擎也对async/await做了很多语法封装,所以了解其背后的代码,有助于加深对async/await的理解。async/await无疑是异步编程领域非常大的创新,也是未来的主流编程风格。其实除了JavaScript之外,Python、Dart、C#等语言也都引入了async/await。使用它不仅可以让代码更加整洁美观,还可以保证函数总能返回Promise。异步编程小结虽然早期的异步回调函数解决了同步阻塞的问题,但是很容易写出回调地狱。Generator生成器最大的特点就是可以控制函数的执行,是协程的一种实现。async/await可以说是异步编程的终极解决方案。它以同步方式编写异步代码。可以把await看成是放开线程的标志,先执行async函数外面的代码,等调用栈为空的时候再返回call。等待背后的代码。