Async-Await ≈ Generators + Promises

原文链接 Async-Await ≈ Generators + Promises

这篇文章主要向你阐述:为什么说 ES2017 中的 async 函数其实是 ES2016 中的特性 generator (生成器) 和 promise 之间的 “互动游戏”

勘误:原文说 async 是 ES2017 的特性,而 generator 和 promise 是 ES2016 的特性。事实上,async 是 ES7(即 ES2016) 而 generator/promise 是 ES6(即 ES2015),作者可能有点混淆
ECMAScript 2015 support in Mozilla
ECMAScript 2016 to ES.Next support in Mozilla

开始之前…

  • 本文并是介绍 promisegeneratorasync 函数的用法
  • 本文的唯一主题是介绍如何用 generator 和 promise 实现 async 函数
  • 笔者对于 async 是否比其他方式更好持中立态度
  • 文章中的代码示例为方便阐述问题,实际业务中引用要慎重

为什么需要理解 async

大多数平台(比如浏览器和 Node )已经可以很好的支持 async 函数了,那么我们为什么还需要理解其实现原理呢?

好吧,除了好奇心以外,另外一个重要原因是一些旧版本的平台可能并不支持某些新特性. 比如程序运行在旧版本的浏览器或者 Node.js 上,但是你又想在开发中使用 async 特性。那么你可能会被要求使用一些工具比如 Babel 对代码中进行转译(transform)。

因此,掌握 async 函数分解(decompose)为 generator 和 promise 的原理对我们阅读或者调试由 Babel 对 async 转译后的代码很有帮助。举个非常简单的栗子:
Async-Await ≈ Generators + Promises_第1张图片
上述这个 async 将会被 Babel 转译成如下 ES6 代码 (不用担心看不明白,我会在接下来的内容讲解)
Async-Await ≈ Generators + Promises_第2张图片
转译前后的代码看起来如此的不同!但是如果你真正理解了 async 的原理,转译代码做了什么是一目了然的。
另外一个有趣的事实是,各种浏览器对 async 函数的实现方式与 Babel 对 async 的转译方式也是类似的

接下来,见证奇迹的时刻

有时候为了弄清楚事情的原理,最好的方式是自己动手做一遍。我们再来看一下这道考题:

我们的代码片段是 async 函数,我们如何用 generator 和 promise 重写它?
Async-Await ≈ Generators + Promises_第3张图片
上述 async 函数中包含三个异步任务,每一个任务都依赖上一个任务的返回结果,最终会返回最后一个任务的运行结果。

如何使用 generator?

生成器函数在执行时能暂停,后面又能从暂停处继续执行。
我们先来看一个生成器函数的简单栗子:
Async-Await ≈ Generators + Promises_第4张图片
上述这个生成器函数 gen 有几个有趣的点 (MDN文档中也有提及):

  1. 生成器函数在调用时,函数体不会立即执行。而是会返回一个遵循迭代器协议的迭代器对象,该对象拥有一个 next 方法。
  2. 执行函数体的唯一方法是调用迭代器对象的 next 方法。next 会执行函数体直到遇到 下一个 yield 表达式。此时 next 会返回该 yield 表达式的值。
  3. 当迭代器对象带参调用next方法,生成器会将这个参数作为表达式整体的返回值,随后继续向下执行直到遇到下一个yield表达式

generator 总结

  • 生成器函数的执行方式是 yield-by-yield (即一次执行一个 yield 表达式)。通过迭代器对象的 next 方法来触发。
  • 每个 yield 的行为模式可以总结为 传递 -> 停止 -> 获取 (give -> halt -> take),具体含义如下:
  • 当前 yield 表达式的值会传递给迭代器对象(iterator)
  • 生成器函数会在当前 yield 处暂停执行,直到迭代器对象的 next 方法再次被调用。
  • 当 next 再次被调用时,当前 yield 会获取 next 中的参数(译者注:如果有的话),用参数值替换暂停执行处 yield 表达式的值。然后继续执行直到下一个 yield。

想了解更多的信息当然还是参考MDN;

这对我们有何帮助?

你也许会问,generator 对我们的业务场景有何帮助?当我们创建异步流时,我们必须等待某些异步任务完成之后才能进行下一步。但是目前为止,我们讨论的都是同步的情景。我们怎么用同步代码来实现异步场景?

最重要的实现细节就是:生成器可以 yield Promise!

比如说 generator 函数中的异步任务为 Promise。我可以控制迭代器 iterator 停止直到 Promise 触发 resolve 或 reject,然后将 Promise 返回的值作为参数继续进行迭代(next)。这种将迭代器与 Promise 混用的模式可以满足如下需求:
Async-Await ≈ Generators + Promises_第5张图片
根据上述代码注释中的描述,即生成器函数 generator yield 一个 Promise,只能算完成了一半工作。我们还需要一个函数来完成如下工作:控制 generator 函数中的迭代器对象 iterator,使其在 yield Promise 时暂停遍历,当 Promise 状态变化即 resolve 或 reject 时继续遍历。

听起来很复杂?实现起来其实很简单。如下所示:
Async-Await ≈ Generators + Promises_第6张图片
现在我们就可以结合使用生成器函数 init 和 runner 了:
Async-Await ≈ Generators + Promises_第7张图片
任务完成了!runner 和 generator 的结合使用收获了与 async 函数类似的效果。

注意: runner 函数只是一个用来阐述概念的 demo,并不严谨。它并不适合在生产环境中直接使用。如果你想找一个更好的实现可以参考这里

总结

我们从 async 函数开始,然后用 generator 和 promise 实现几乎相同的效果。对比如下:
Async-Await ≈ Generators + Promises_第8张图片

进一步练习

  • 在文章的开头,我们讲解了 babel 是如何将 async 函数转译为 ES6 中的 generator 和 promsie。通过 babel 的转译代码中的 _asyncToGenerator 和我们刚刚实现的 runner,你会发现二者何其的相似。事实上,你可以把 _asyncToGenerator 看作是 runner 的严谨实现。
  • 如果你有进一步学习的兴趣,你可以试着将async 可以转译成不使用 generator 的 ES6 代码。你可以参照这里regenerator project. (在无限循环的while中使用 switch 模拟实现 generator)

我希望这篇文章可以帮你更好的理解 async 背后的实现机制. 这种机制通过提供简洁的语法使代码更加清晰易读。正如 async 函数提案中所述:

Promise 和 Generator 的引入在语言层面上极大的提高了 ECMAScript 编写异步代码的能力

The introduction of Promises and Generators in ECMAScript presents an opportunity to dramatically improve the language-level model for writing asynchronous code in ECMAScript.

最后谢谢Akos,Alisa 和 Kristian 为本文提供的反馈和帮助。

你可能感兴趣的:(ES6,Async,Await,Generator,Promise,JS)