原文地址

引言

无论是面试过程还是日常业务开发,相信大多数前端开发者可以熟练使用 Async/Await 作为异步任务的终极处理方案。
但是对于 Async 函数的具体实现过程只是知其然不知所以然,仅仅了解它是基于 Promise 和 Generator 生成器函数的语法糖。
提及 JavaScript 中 Async 函数的内部实现原理,大多数开发者并不清楚这一过程。甚至从来没有思考过 Async 所谓语法糖是如何被 JavaScript 组合而来的。
别担心,文中会带你分析 Async 语法在低版本浏览器下的 polyfill 实现,同时我也会手把手带你基于 Promise 和 Generator 来实现所谓的 Async 语法。
我们会从以下方面来逐步攻克 Async 函数背后的实现原理:

  • 🌟 Promise 章节梳理,从入门到源码带你掌握 Promise 应用。
  • 🌟 什么是生成器函数?Generator 生成器函数基本特征梳理。
  • 🌟 Generator 是如何被实现的,Babel 如何在低版本浏览器下实现 Generator 生成器函数。
  • 🌟 作为通用异步解决方案的 Generator 生成器函数是如何解决异步方案。
  • 🌟 开源 Co 库的基本原理实现。
  • 🌟 Async/Await 函数为什么会被称为语法糖,它究竟是如何被实现的。

相信读完文章的你,对于 Async/Await 真正可以做到“知其然,知其所以然”。

Promise

所谓 Async/Await 语法我们提到本质上它是基于Promise 和 Generator 生成器函数的语法糖
关于 Promise 这篇文章中我就不过于展开他的基础和原理部分了,网络中对于介绍 Promise 相关的文章目前已经非常优秀了。如果有兴趣深入 Promise 的同学可以查看:

Promise 基础使用准则,MDN 上给出了详尽的说明和实例,强烈建议对于 Promise 陌生的同学可以查阅 MDN 巩固 Promise 基础知识。

Promise A+ 实现准则,不同浏览器/环境下对于 Promise 都有自己的实现,它们都会依照同一规范标准去实现 Promise 。
我在 ➡️ 这个地址按照规范实现过一版完整的 Promise ,有兴趣的通许可以自行查阅代码进行 Promise 原理巩固。

关于 Promise 中各种边界应用以及深层次 Promise 原理实现,笔者强烈建议有兴趣更深层次的同学结合月夕的这
篇文章去参照阅读。

生成器函数

关于 Generator 生成器函数与 Iterator 迭代器,大多数开发者在日常应用中可能并不如 Promise 那么常见。
所以针对于 Generator 我会稍微和大家从基础开始讲起。

Generator 概念

Generator 基础入门

所谓 Generator 函数它是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即拥有暂停函数执行的效果)。

  1. function* gen() {
  2. yield 1;
  3. yield 2;
  4. yield 3;
  5. }
  6. let g = gen();
  7. g.next(); // { value: 1, done: false }
  8. g.next(); // { value: 2, done: false }
  9. g.next(); // { value: 3, done: false }
  10. g.next(); // { value: undefined, done: true }
  11. g.next(); // { value: undefined, done: true }

上述的函数就是一个 Generator 生成器函数的例子,我们通过在函数声明后添加一个 的语法创建一个名为 gen 的生成器函数。
调用创建的生成器函数会返回一个 Generator { } *生成器实例对象

初次接触生成器函数的同学,看到上面的例子可能稍微会有点懵。什么是 Generator 实例对象,函数中的 yield 关键字又是做什么的,我们应该如何使用它呢?
别着急,接下来我们来一步一揭开这些迷惑。
所谓返回的 g 生成器对象你可以简单的将它理解成为类似这样的一个对象结构

  1. {
  2. next: function () {
  3. return {
  4. done:Boolean, // done表示生成器函数是否执行完毕 它是一个布尔值
  5. value: VALUE, // value表示生成器函数本次调用返回的值
  6. }
  7. }
  8. }
  • 首先,我们通过 let g = gen() 调用生成器函数创建了一个生成器对象 g ,此时 g 拥有 next 上述结构的 next 方法。

这一步,我们成为 g 为返回的生成器对象, gen 为生成器函数。通过调用生成器函数 gen 返回了生成器对象 g

  • 之后,生成器对象中的 next 方法每次调用会返回一次 { value:VALUE, done:boolean }的对象。

每次调用生成器对象的 next 方法会返回一个上述类型的 object:
其中 done 表示生成器函数是否执行完毕,而 value 表示生成器函数中本次 yield 对应的值。
部分没有接触过的同学可能不太了解这一过程,我们来详细拆开上述函数的执行过程来看看:

  • 首先调用 gen() 生成器函数返回 g 生成器对象。
  • 其次返回的 g 生成器对象中拥有一个 next 的方法。
  • 每当我们调用 g.next() 方法时,生成器函数紧跟着上一次进行执行,直到函数碰到 yield 关键值。
    • yield 关键字会停止函数执行并将 yield 后的值返回作为本次调用 next 函数的 value 进行返回。
    • 同时,如果本次调用 g.next() 导致生成器函数执行完毕,那么此时 done 会变成 true 表示该函数执行完毕,反之则为 false 。

比如当我们调用 let g = gen() 时,会返回一个生成器函数,它拥有一个 next 方法。
之后当第一次调用 g.next() 方法时,会执行生成器函数 gen 。函数会进行执行,直到碰到 yield 关键字会进行暂停,此时函数会暂停到 yield 1 语句执行完毕,将 1 赋给 value
同时因为生成器函数 gen 并没有执行完毕,所以此时 done 应该为 false 。所以此时首次调用 g.next() 函数返回的应该是 { value: 1, done: false }。
之后,我们第二次调用 g.next() 方法时,函数会从上一次的中断结果后进行执行。也就是会继续 yield 2 语句。
当遇到 yield 2 时,又因为碰到了 yield 语句。此时函数又会被中断,因为此时函数并没有执行完成,并且yield 语句后紧挨着的是 2 所以第二个 g.next() 会返回 { value: 2 , done: false }。
同样,yield 3; 回和前两次执行逻辑相同。
需要额外注意的是,当我们第四次调用迭代器 g.next() 时,因为第三次 g.next() 结束时生成器函数已经执行完毕了。所以再次调用 g.next() 时,由于函数结束 done 会变为 false 。同时因为函数不存在返回值,所以 value 为 undefined。
上边是一个基于 Generator 函数的简单执行过程,其实它的本质非常简单:
调用生成器函数会返回一个生成器对象,每次调用生成器对象的 next 方法会执行函数到下一次 yield 关键字停止执行,并且返回一个 { value: Value, done: boolean }的对象。
上述执行过程,我稍稍用了一些篇幅来描述这一简单的过程。如果看到这里你还是没有上面的 Demo 含义,那么此时请你停下往下的进度,会到开头一定要搞清楚这个简单的 Demo 。

Generator 函数返回值

在掌握了基础 Generator 函数和 yield 关键字后,趁热打铁让我们来一举攻克 Generator 生成器函数的进阶语法。
老样子,我们先来看这样一段代码:

  1. function* gen() {
  2. const a = yield 1;
  3. console.log(a,'this is a')
  4. const b = yield 2;
  5. console.log(b,'this is b')
  6. const c = yield 3;
  7. console.log(c,'this is c')
  8. }
  9. let g = gen();
  10. g.next(); // { value: 1, done: false }
  11. g.next('param-a'); // { value: 2, done: false }
  12. g.next('param-b'); // { value: 3, done: false }
  13. g.next('param-c'); // { value: undefined, done: true }
  14. // 控制台会打印:
  15. // param-a this is a
  16. // param-b this is b
  17. // param-c this is c

这里,我们着重来看看调用生成器对象的 next 方法传入参数时究竟会发生什么事情,理解 next() 方法的参数是后续 Generator 解决异步的重点实现思路。
上文我们提到过,生成器函数中的 yield 关键字会暂停函数的运行,简单来说比如我们第一次调用 g.next() 方法时函数会执行到 yield 1 语句,此时函数会被暂停。
当第二次调用 g.next() 方法时,生成器函数会继续从上一次暂停的语句开始执行。这里有一个需要注意的点:当生成器函数恢复执行时,因为上一次执行到 const a = yield 1 语句的右半段并没有给 const a进行赋值。
那么此时的赋值语句 const a = yield 1,a 会被赋为什么值呢? 细心的同学可能已经发现了。我们在 g.next(‘param-a’) 传入的参数 param-a 会作为生成器函数重新执行时,上一次 yield 语句的返回值进行执行。
简单来说,也就是调用 g.next(‘param-a’)恢复函数执行时,相当于将生成器函数中的 const a = yield 1; 变成 const a = ‘param-a’; 进行执行。
这样,第二次调用 g.next(‘param-a’)时自然就打印出了 param-a this is a 。
同样当我们第三次调用 g.next(‘param-b’) 时,本次调用 next 函数传入的参数会被当作 yield 2 运算结果赋值给 b 变量,执行到打印时会输出 param-b this is b。
同理 g.next(‘paramc’) 会输出 param-c this is b。
总而言之,当我们为 next 传递值进行调用时,传入的值会被当作上一次生成器函数暂停时 yield 关键字的返回值处理。
自然,第一次调用 g.next() 传入参数是毫无意义的。因为首次调用 next 函数时,生成器函数并没有任何执行自然也没有 yield 关键字处理。
接下来我们来看看所谓的生成器函数返回值:

  1. function* gen() {
  2. const a = yield 1;
  3. console.log(a, 'this is a');
  4. const b = yield 2;
  5. console.log(b, 'this is b');
  6. const c = yield 3;
  7. console.log(c, 'this is c');
  8. return 'resultValue'
  9. }
  10. let g = gen();
  11. g.next(); // { value: 1, done: false }
  12. g.next('param-a'); // { value: 2, done: false }
  13. g.next('param-b') // { value: 3, done: false }
  14. g.next() // { value: 'resultValue', done: true }
  15. g.next() // { value: undefined, done: true }

当生成器函数存在 return 返回值时,我们会在第四次调用 g.next() 函数恢复执行,此时生成器函数继续执行函数执行完毕。
此时自然 done 会变为 true 表示生成器函数已经执行完毕,之后,由于函数存在返回值所以随之本次的 value 会变为 ‘resultValue’ 。
也就是当生成器函数执行完毕时,原本本次调用 next 方法返回的 {done:true,value:undefined} 变为了{ done:true,value:’resultValue’}。
关于 Generator 函数的基本使用我们就介绍到这里,接下来我们来看看它是如何被 JavaScript 实现的。

Generator 原理实现

关于 Generator 函数的原理其实和我们后续的异步关系并不是很大,但是本着“知其然,知其所以然”的出发点。
希望大家可以耐心去阅读本小结,其实它的内部运行机制并不是很复杂。笔者自己也在某电商大厂面试中被问到过如何实现 Generator 的 polyfill。
首先,你可以打开链接查看我已经编辑好的 ➡️ Babel Generator Demo
2022/03/30 【Async是如何被JavaScript实现的】 - 图1
乍一看也许很多同学会稍微有点懵,没关系。这段代码并不难,难的是你对未知恐惧的心态。
这是 Babel 在低版本浏览器下为我们实现的 Generator 生成器函数的 polyfill 实现。

左侧为 ES6 中的生成器语法,右侧为转译后的兼容低版本浏览器的实现。
首先左侧的 gen 生成器函数被在右侧被转化成为了一个简单的普通函数,具体 gen 函数内容我们先忽略它。
在右侧代码中,对于普通 gen 函数包裹了一层 regeneratorRuntime.mark(gen) 处理,在源码中这一步其实为了将普通 gen 函数继承 GeneratorFunctionPrototype 从而实现将 gen() 返回的对象变成 Generator 实例对象的处理。
这一步对于我们理解 Generator 显得并不是那么重要,所以我们可以简单的将 regeneratorRuntime.mark 改写成为这样的结构:

  1. // 自己定义regeneratorRuntime对象
  2. const regeneratorRuntime = {
  3. // 存在mark方法,接受传入的fn。原封不懂的返回fn
  4. mark(fn) {
  5. return fn
  6. }
  7. }

我们自己定义了 regeneratorRuntime 对象,并且为他定义了一个 mark 方法。它的作用非常简单,接受一个函数作为入参并且返回函数,仅此而已。
之后我们再来进入 gen 函数内部,在左侧源代码中当我们调用 gen() 时,是会返回一个 Iterator 对象(它拥有 next 方法,并且每次调用 next 都会返回 {value:VALUE,done:boolean})。
所以在右侧我们了解到了,我们通过调用编译后的普通 gen() 函数应该和右侧返回一致,所谓 regeneratorRuntime.wrap() 方法也应该返回一个具有 next 属性的迭代器对象。
关于 regeneratorRuntime.wrap() 方法,这里传入了两个参数,第一个参数是一个接受传入 context 的函数,第二个参数是我们之前处理的 _marked 对象。
同样关于 wrap() 方法的第二个参数我们不用过多纠结,它仍然是对于编译后的生成器函数作为继承使用的一个参数,并不影响函数的核心逻辑。所以我们暂时忽略它。
此时,我们了解到,regeneratorRuntime 对象上应该存在一个 wrap 方法,并且 wrap 方法应该返回一个满足迭代器协议的对象。

  1. // 自己定义regeneratorRuntime对象
  2. const regeneratorRuntime = {
  3. // 存在mark方法,接受传入的fn。原封不懂的返回fn
  4. mark(fn) {
  5. return fn
  6. },
  7. wrap(fn) {
  8. // ...
  9. return {
  10. next() {
  11. done: ...,
  12. value: ...
  13. }
  14. }
  15. }
  16. }

让我们进入 regeneratorRuntime.wrap 内部传入的具体函数来看看: