如何实现 async/await
基于Promise的异步处理虽然解决了基于callback的过多嵌套的问题,但是可读性也还是比较差,流程控制也不是特别方便。所以ES7提出了async函数完美解决了上述问题。探究其原理async/await实际上是对Generator的封装,是一个语法糖,只不过generator出现不久后就被async/await取代了。
ES6 新引入了 Generator 函数,可以通过 yield 关键字,把函数的执行流挂起,通过 next()方法可以切换到下一个状态,为改变执行流程提供了可能,从而为异步编程提供解决方案。
可以看出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) { |
对于之前的例子,我们就可以这样执行了:
这样我们就初步实现了一个async/await
上边的代码只有五六行,但并不是一下就能看明白的,我们之前用了四个例子来做铺垫,也是为了让读者更好地理解这段代码。简单的说,我们封装了一个 run 方法,run 方法里我们把执行下一步的操作封装成 step(),每次 Promise.then()的时候都去执行 step(),实现自动迭代的效果。在迭代的过程中,我们还把 resolve 的值传入gen.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 对象储存上下文,使得每次执行生成器函数的时候,都可以从上一个执行结果开始执行,看起来就像函数被挂起了一样.