前言我们都知道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{
