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

JavaScript引擎是如何实现async-await的

时间:2023-03-26 22:21:52 JavaScript

前言我们都知道Promise可以很好的解决回调地狱的问题,但是这个方法充满了Promise的then()方法。如果处理流程比较复杂,那么整段代码就会充满then,语义不明显,代码也不能很好的表达执行过程。使用promise.then也相当复杂。虽然整个请求流程已经线性化,但是代码中包含了大量的then函数,使得代码仍然不太好读。为此,ES7引入了async/await,这是对JavaScript异步编程的重大改进,提供了使用同步代码异步访问资源而不阻塞主线程的能力,使代码逻辑更加清晰。如果觉得本文对您有帮助,请用您致富的小手为我点个赞。非常感谢!JavaScript引擎如何实现异步/等待。如果你上来直接介绍如何使用async/await,那你可能会有些迷茫,所以我们将从技术的最低点一步步讲解,带你彻底了解async和await是如何工作的。首先介绍生成器(Generator)的工作原理,然后讲解生成器的底层实现机制——协程(Coroutine);又因为async/await使用了Generator和Promise两种技术,那么我们就使用Generator和Promise来分析async/await是如何以同步的方式编写异步代码的。GeneratorVSCoroutine生成器函数是带星号的函数,可以暂停和恢复执行。function*genDemo(){console.log("开始执行第一段")yield'generator2'console.log("开始执行第二段")yield'generator2'console.log("开始执行第三段")yield'generator2'console.log("Endofexecution")return'generator2'}console.log('main0')letgen=genDemo()console.log(gen.next().value)控制台.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这个函数并没有一次性执行,全局代码和genDemo函数交替执行。其实这就是生成器函数的特点,可以暂停执行和恢复执行。我们来看看生成器函数的具体用法:在生成器函数内部执行一段代码。如果遇到yield关键字,JavaScript引擎会将关键字后面的内容返回给外部,并暂停函数的执行。外部函数可以通过next方法恢复函数执行。关于函数的暂停和恢复,相信大家一定很好奇其中的原理,下面简单介绍一下JavaScript引擎V8是如何实现函数的暂停和恢复的,也有助于大家理解后面的Introducingasync/await。要理解为什么函数可以挂起和恢复,首先要理解协程的概念。协程比线程更轻量级。您可以将协程视为在线程上运行的任务。一个线程上可以有多个协程,但一个线程上同时只能执行一个协程。那么A协程需要将主线程的控制权交给B协程,体现在A协程暂停执行,B协程恢复执行;同样,A协程也可以从B协程启动。通常,如果协程B是从协程A启动的,我们称协程A为协程B的父协程。正如一个进程可以有多个线程一样,一个线程也可以有多个协程。最重要的是,协程不由操作系统内核管理,而完全由程序控制(即在用户态执行)。这样做的好处是性能有了很大的提升,不会像线程切换那样消耗资源。为了让大家更好的理解协程是如何执行的,我根据以上代码的执行过程绘制了如下“协程执行流程图”,大家可以对照代码进行分析:从图中可以看出四协程的点规则:通过调用生成器函数genDemo来创建一个协程gen。创建后,gen协程不会立即执行。要让gen协程执行,需要调用gen.next。协程执行时,可以使用yield关键字暂停gen协程的执行,将主要信息返回给父协程。如果在协程执行过程中遇到return关键字,JavaScript引擎会结束当前协程,将return后的内容返回给父协程。但是,对于上面的代码,你可能会有另外一个疑问:父协程有自己的调用栈,gen协程也有自己的调用栈。当gen协程通过yield将控制权交给父协程时,V8是如何切换到父协程的调用栈的呢?当父协程通过gen.next恢复gen协程时,如何切换gen协程的调用栈?要理解上面的问题,需要注意以下两点。第一点:gen协程和父协程是在主线程上交互执行的,不是并发的。他们之前的切换是通过yield和gen.next的配合完成的。第二点:在gen协程中调用yield方法时,JavaScript引擎会保存gen协程当前的调用栈信息,并恢复父协程的调用栈信息。同理,在父协程中执行gen.next时,JavaScript引擎会保存父协程的调用栈信息,并恢复gen协程的调用栈信息。为了直观的理解父协程和gen协程是如何切换调用栈的,相信你已经弄清楚协程的工作原理了。其实在JavaScript中,生成器就是协程的一种实现,所以相信你也明白什么是生成器。那么接下来,我们使用generators和Promises对一开始的Promise代码进行改造。修改后的代码如下://foofunctionfunction*foo(){letresponse1=yieldfetch('https://www.geekbang.org')console.log('response1')console.log(response1)letresponse2=yieldfetch('https://www.geekbang.org/test')console.log('response2')console.log(response2)}//执行foo函数的代码letgen=foo()functiongetGenPromise(gen){returngen.next().value}getGenPromise(gen).then((response)=>{console.log('response1')console.log(response)返回getGenPromise(gen)}).then((response)=>{console.log('response2')console.log(response)})复制代码从图中可以看出,foo函数是一个生成器函数,同步代码在foo中实现function实现异步操作;但是在foo函数之外,我们还需要写一段代码来执行foo函数,如上面代码的后半部分所示,那么我们来分析一下这段代码是如何工作的。首先要执行的是letgen=foo(),它创建了gen协程。然后通过在父协程中执行gen.next,将主线程的控制权交给gen协程。gen协程获得主线程的控制权后,调用fetch函数创建一个Promise对象response1,然后通过yield暂停gen协程的执行,将response1返回给父协程。父协程恢复执行后,调用response1.then方法等待请求结果。fetch发起的请求完成后,会调用then中的回调函数。then中的回调函数得到结果后,会通过调用gen.next放弃主线程的控制权,将控制权交给gen协程继续执行下一个ask。以上就是coroutines和Promise合作的大概流程。但是通常,我们把执行generator的代码封装成一个函数,把这个执行generator代码的函数称为executor(参考大名鼎鼎的co框架),如下:function*foo(){letresponse1=yieldfetch('https://www.geekbang.org')console.log('response1')console.log(response1)letresponse2=yieldfetch('https://www.geekbang.org/test')console.log('response2')console.log(response2)}co(foo());复制代码通过将生成器与执行器一起使用,您可以以同步的方式编写异步代码,这也大大增强了代码的可读性。虽然async/await生成器已经可以很好的满足我们的需求,但是程序员的追求是无止境的。这在ES7中没有引入async/await,可以彻底告别executors和generators,实现更直观简洁的代码。其实async/await技术背后的秘密是Promise和generator的应用,更底层是microtasks和coroutines的应用。要理解async和await是如何工作的,我们必须分别分析async和await。async下面我们来看看async到底是什么?根据MDN,async是一个异步执行并隐式返回Promise作为结果的函数。这里我们先看看如何隐式返回Promise,可以参考下面的代码:asyncfunctionfoo(){return2}console.log(foo())//Promise{:2}复制代码Executingthis代码,我们可以看到调用async声明的foo函数返回了一个Promise对象,状态为resolved,返回结果如下:Promise{:2}复制代码await我们知道async函数返回了APromise对象,那我们就结合文章中的这段代码,看看await是什么。asyncfunctionfoo(){console.log(1)leta=await100console.log(a)console.log(2)}console.log(0)foo()console.log(3)复制代码观察以上能看出打印的内容是什么吗?这就得先分析async和await结合起来会发生什么。在详细介绍之前,我们先从协程的角度来看一下这段代码的整体执行流程图:结合上图,我们一起来分析一下async/await的执行流程。首先执行console.log(0)语句,打印出0。接下来就是执行foo函数了。由于foo函数被标记为async,当进入该函数时,JavaScript引擎会保存当前的调用堆栈等信息,然后执行foo函数中的console.log(1)语句。并打印出1.接下来执行foo函数中的await100语句。这是我们分析的重点,因为在执行await100语句的时候,JavaScript引擎已经在幕后默默地为我们做了太多的事情,所以我们把这条语句拆开来看看JavaScript做了什么。当执行到await100时,会默认创建一个Promise对象。代码如下letpromise_=newPromise((resolve,reject){resolve(100)})复制代码在创建这个promise_对象的过程中,我们可以看到在executor函数中调用resolve函数时,JavaScript引擎将任务提交到微任务队列。然后JavaScript引擎会暂停当前协程的执行,将主线程的控制权交给父协程执行,并将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在幕后为我们做了很多工作,所以我们可以用同步的方式来写异步代码。小结Promise的编程模型还是充斥着大量的then方法。虽然解决了回调地狱的问题,但是在语义上仍然存在缺陷。代码中充斥着大量的then函数,这也是async/await出现的原因。使用async/await可以写出同步代码风格的异步代码。这是因为async/await的基础技术使用了生成器和承诺。生成器是协程的实现。可以使用生成器暂停生成器函数。并恢复。另外,V8引擎也对async/await做了很多语法封装,所以了解其背后的代码,有助于加深对async/await的理解。async/await无疑是异步编程领域非常大的创新,也是未来的主流编程风格。其实除了JavaScript之外,Python、Dart、C#等语言也都引入了async/await。使用它不仅可以让代码更加整洁美观,还可以保证函数总能返回Promise。最后,如果您觉得这篇文章对您有点帮助,请点个赞。或者可以加入我的开发交流群:1025263163互相学习,我们会有专业的技术解答。如果您觉得这篇文章对您有用,请给我们的开源项目一个小星星:http://github。crmeb.net/u/defu非常感谢!完整源码下载地址:https://market.cloud.tencent....PHP学习手册:https://doc.crmeb.com技术交流论坛:https://q.crmeb.com