如何实现 async/await
基于 Promise
的异步处理虽然解决了基于 callback 的过多嵌套的问题,但是可读性也并没有强多少,流程控制也不是特别方便,所以 ES7 提出了 async
函数完美解决了上述问题。async/await实际上是对Generator 的封装,是生成器函数的语法糖。
async
函数是AsyncFunction
构造函数的实例,并且在函数体中允许使用 await
关键字。await
操作符用于等待 Promise 兑现并获取它兑现之后的值,智能在 async 函数或者顶层模块中使用。async
和 await
关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用 Promise
ES6 新引入了 Generator 函数,可以通过
yield
关键字,把函数的执行流程挂起,通过next()
方法可以切换到下一个状态,为改变执行流程提供了可能,从而为异步编程提供解决方案。
yield
表达式本身没有返回值(或者总是返回undefined
),next
方法可以带一个参数,该参数就会被当作上一个yield
表达式的返回值。这个功能有非常重要的语法意义。生成器函数从暂停状态到恢复运行,它的上下文状态(context)是不变的,通过next
方法的参数,就有办法在生成器函数开始运行后,继续向函数体内部注入值。—— 可以在生成器函数运行的不同阶段,通过next
传递参数注入不同的值,从而调整函数的行为。
注意:由于
next
方法的参数表示上一个yield
表达式的返回值,因此第一次调用next
方法的时候传递参数是无效的(引擎会直接忽略掉),从语义上来说第一次调用next
函数是用来启动遍历器对象,是不能带参数的。
1 | function* foo(x) { |
1 | function* mygen(){ |
可以看出yield
和async/await
已经非常相似了,他们都提供了暂停执行的功能,但是二者又有以下几点不同:
async/await
自带执行器,不需要手动调用next()
就能自动执行下一步async
函数返回值是 Promise 对象,而 Generator 返回的是生成器对象await
能够返回 Promise 的 resolve/reject 的值
我们对 async/await
的实现,其实也就是对应以上三点封装 Generator
自动执行
我们先来看一下,对于这样一个 Generator,手动执行是怎样一个流程
1 | function* myGenerator() { |
我们也可以通过给 gen 生成器的 next
方法传值的方式,让yield
能返回 resolve 的值
1 | function* myGenerator() { |
显然,手动执行的写法看起来既笨拙又丑陋,我们希望生成器函数能自动往下执行,且yield
能返回 resolve 的值,基于这两个需求,我们进行一个基本的封装,由于async/await
是关键字,无法重写,我们用函数来模拟:
1 | function run(gen) { |
对于之前的例子,我们就可以这样执行了:
1 | function* myGenerator() { |
这样我们就初步实现了一个 async/await
上边的代码只有五六行,但并不是一下就能看明白的,我们之前用了四个例子来做铺垫,也是为了让读者更好地理解这段代码。简单的说,我们封装了一个 runGen 方法,runGen 方法里我们把执行下一步的操作封装成 next(),每次 Promise.then() 的时候都去执行 next(),实现自动迭代的效果。在迭代的过程中,我们还把 resolve 的值传入 it.next,使得 yield
得以返回 Promise 的 resolve 的值。
返回 Promise & 异常处理
虽然我们实现了 Generator 的自动执行以及让 yield
返回 resolve 的值,但上边的代码还存在着几点问题:
- 需要兼容基本类型:这段代码能自动执行的前提是
yield
后面跟Promise
,为了兼容后面跟着基本类型值的情况,我们需要把yield
跟的内容(gen().next.value)
都用Promise.resolve()
转化一遍 - 缺少错误处理:上边代码里的
Promise
如果执行失败,就会导致后续执行直接中断,我们需要通过调用Generator.prototype.throw()
,把错误抛出来,才能被外层的try-catch
捕获到 - 返回值是Promise:
async/await
的返回值是一个Promise
,我们这里也需要保持一致,给返回值包一个Promise
改造后的 run
方法:
1 | function run(gen) { |
测试结果如下:
到这里,一个基本的async/await
就实现完成了,但是直到结尾,我们也不知道await
到底是如何暂停执行的,有关await
暂停执行的秘密,我们还要到 Generator 的实现中去寻找答案。
Generator的实现
例子如下:
1 | function* foo() { |
我们可以在babel官网找到其在ES5下的实现:
代码咋一看不长,但如果仔细观察会发现有两个不认识的东西 —— regeneratorRuntime.mark
和regeneratorRuntime.wrap
,这两者其实是 regenerator-runtime
模块里的两个方法,regenerator-runtime
模块来自 facebook 的 regenerator
模块。直接看源码是有点懵,接下来我们实现一下它的低配版:
1 | // 生成器函数根据yield语句将代码分割为switch-case块,后续通过切换_context.prev和_context.next来分别执行各个case |
从中我们可以看出,Generator 实现的核心在于上下文的保存,这其实是一个状态机。函数并没有真的被挂起,每一次yield
,其实都执行了一遍传入的生成器函数,只是在这个过程中间用了一个context
对象储存上下文,使得每次执行生成器函数的时候,都可以从上一个执行结果开始执行,看起来就像函数被挂起了一样。