前ES6时代的Generator

我希望你已经被说服了,generator是一个异步编程工具箱里的非常重要的增强工具。但它是ES6中的新语法,这意味着你不能像填补Promise(它只是新的API)那样填补generator。那么如果我们不能奢望忽略前ES6时代的浏览器,我们该如何将generator带到浏览器中呢?

对所有ES6中的新语法的扩展,有一些工具——称呼他们最常见的名词是转译器(transpilers),也就是转换编译器(trans-compilers)——它们会拿起你的ES6语法,并转换为前ES6时代的等价代码(但是明显地变难看了!)。所以,generator可以被转译为具有相同行为但可以在ES5或以下版本进行工作的代码。

但是怎么做到的?yield的“魔法”听起来不像是那么容易转译的。在我们早先的基于闭包的 迭代器 例子中,实际上提示了一种解决方法。

手动变形

在我们讨论转译器之前,让我们延伸一下,在generator的情况下如何手动转译。这不仅是一个学院派的练习,因为这样做实际上可以帮助我们进一步理解它们如何工作。

考虑这段代码:

  1. // `request(..)` 是一个支持Promise的Ajax工具
  2. function *foo(url) {
  3. try {
  4. console.log( "requesting:", url );
  5. var val = yield request( url );
  6. console.log( val );
  7. }
  8. catch (err) {
  9. console.log( "Oops:", err );
  10. return false;
  11. }
  12. }
  13. var it = foo( "http://some.url.1" );

第一个要注意的事情是,我们仍然需要一个可以被调用的普通的foo()函数,而且它仍然需要返回一个 迭代器。那么让我们来画出非generator的变形草图:

  1. function foo(url) {
  2. // ..
  3. // 制造并返回 iterator
  4. return {
  5. next: function(v) {
  6. // ..
  7. },
  8. throw: function(e) {
  9. // ..
  10. }
  11. };
  12. }
  13. var it = foo( "http://some.url.1" );

下一个需要注意的地方是,generator通过挂起它的作用域/状态来施展它的“魔法”,但我们可以用函数闭包来模拟。为了理解如何写出这样的代码,我们将先用状态值注释generator不同的部分:

  1. // `request(..)` 是一个支持Promise的Ajax工具
  2. function *foo(url) {
  3. // 状态 *1*
  4. try {
  5. console.log( "requesting:", url );
  6. var TMP1 = request( url );
  7. // 状态 *2*
  8. var val = yield TMP1;
  9. console.log( val );
  10. }
  11. catch (err) {
  12. // 状态 *3*
  13. console.log( "Oops:", err );
  14. return false;
  15. }
  16. }

注意: 为了更准去地讲解,我们使用TMP1变量将val = yield request..语句分割为两部分。request(..)发生在状态*1*,而将完成值赋给val发生在状态*2*。在我们将代码转换为非generator的等价物后,我们就可以摆脱中间的TMP1

换句话所,*1*是初始状态,*2*request(..)成功的状态,*3*request(..)失败的状态。你可能会想象额外的yield步骤将如何编码为额外的状态。

回到我们被转译的generator,让我们在这个闭包中定义一个变量state,用它来追踪状态:

  1. function foo(url) {
  2. // 管理 generator 状态
  3. var state;
  4. // ..
  5. }

现在,让我们在闭包内部定义一个称为process(..)的内部函数,它用switch语句来处理各种状态。

  1. // `request(..)` 是一个支持Promise的Ajax工具
  2. function foo(url) {
  3. // 管理 generator 状态
  4. var state;
  5. // generator-范围的变量声明
  6. var val;
  7. function process(v) {
  8. switch (state) {
  9. case 1:
  10. console.log( "requesting:", url );
  11. return request( url );
  12. case 2:
  13. val = v;
  14. console.log( val );
  15. return;
  16. case 3:
  17. var err = v;
  18. console.log( "Oops:", err );
  19. return false;
  20. }
  21. }
  22. // ..
  23. }

在我们的generator中每种状态都在switch语句中有它自己的case。每当我们需要处理一个新状态时,process(..)就会被调用。我们一会就回来讨论它如何工作。

对任何generator范围的变量声明(val),我们将它们移动到process(..)外面的var声明中,这样它们就可以在process(..)的多次调用中存活下来。但是“块儿作用域”的err变量仅在*3*状态下需要,所以我们将它留在原处。

在状态*1*,与yield request(..)相反,我们return request(..)。在终结状态*2*,没有明确的return,所以我们仅仅return;也就是return undefined。在终结状态*3*,有一个return false,我们保留它。

现在我们需要定义 迭代器 函数的代码,以便人们恰当地调用process(..)

  1. function foo(url) {
  2. // 管理 generator 状态
  3. var state;
  4. // generator-范围的变量声明
  5. var val;
  6. function process(v) {
  7. switch (state) {
  8. case 1:
  9. console.log( "requesting:", url );
  10. return request( url );
  11. case 2:
  12. val = v;
  13. console.log( val );
  14. return;
  15. case 3:
  16. var err = v;
  17. console.log( "Oops:", err );
  18. return false;
  19. }
  20. }
  21. // 制造并返回 iterator
  22. return {
  23. next: function(v) {
  24. // 初始状态
  25. if (!state) {
  26. state = 1;
  27. return {
  28. done: false,
  29. value: process()
  30. };
  31. }
  32. // 成功地让出继续值
  33. else if (state == 1) {
  34. state = 2;
  35. return {
  36. done: true,
  37. value: process( v )
  38. };
  39. }
  40. // generator 已经完成了
  41. else {
  42. return {
  43. done: true,
  44. value: undefined
  45. };
  46. }
  47. },
  48. "throw": function(e) {
  49. // 在状态 *1* 中,有唯一明确的错误处理
  50. if (state == 1) {
  51. state = 3;
  52. return {
  53. done: true,
  54. value: process( e )
  55. };
  56. }
  57. // 否则,是一个不会被处理的错误,所以我们仅仅把它扔回去
  58. else {
  59. throw e;
  60. }
  61. }
  62. };
  63. }

这段代码如何工作?

  1. 第一个对 迭代器next()调用将把gtenerator从未初始化的状态移动到状态1,然后调用process()来处理这个状态。request(..)的返回值是一个代表Ajax应答的promise,它作为value属性从next()调用被返回。
  2. 如果Ajax请求成功,第二个next(..)调用应当送进Ajax的应答值,它将我们的状态移动到2process(..)再次被调用(这次它被传入Ajax应答的值),而从next(..)返回的value属性将是undefined
  3. 然而,如果Ajax请求失败,应当用错误调用throw(..),它将状态从1移动到3(而不是2)。process(..)再一次被调用,这词被传入了错误的值。这个case返回false,所以false作为throw(..)调用返回的value属性。

从外面看——也就是仅仅与 迭代器 互动——这个普通的foo(..)函数与*foo(..)generator的工作方式是一样的。所以我们有效地将ES6 generator“转译”为前ES6可兼容的!

然后我们就可以手动初始化我们的generator并控制它的迭代器——调用var it = foo("..")it.next(..)等等——或更好地,我们可以将它传递给我们先前定义的run(..)工具,比如run(foo,"..")

自动转译

前面的练习——手动编写从ES6 generator到前ES6的等价物的变形过程——教会了我们generator在概念上是如何工作的。但是这种变形真的是错综复杂,而且不能很好地移植到我们代码中的其他generator上。手动做这些工作是不切实际的,而且将会把generator的好处完全抵消掉。

但走运的是,已经存在几种工具可以自动地将ES6 generator转换为我们在前一节延伸出的东西。它们不仅帮我们做力气活儿,还可以处理几种我们敷衍而过的情况。

一个这样的工具是regenerator(https://facebook.github.io/regenerator/),由Facebook的聪明伙计们开发的。

如果我们用regenerator来转译我们前面的generator,这就是产生的代码(在编写本文时):

  1. // `request(..)` 是一个支持Promise的Ajax工具
  2. var foo = regeneratorRuntime.mark(function foo(url) {
  3. var val;
  4. return regeneratorRuntime.wrap(function foo$(context$1$0) {
  5. while (1) switch (context$1$0.prev = context$1$0.next) {
  6. case 0:
  7. context$1$0.prev = 0;
  8. console.log( "requesting:", url );
  9. context$1$0.next = 4;
  10. return request( url );
  11. case 4:
  12. val = context$1$0.sent;
  13. console.log( val );
  14. context$1$0.next = 12;
  15. break;
  16. case 8:
  17. context$1$0.prev = 8;
  18. context$1$0.t0 = context$1$0.catch(0);
  19. console.log("Oops:", context$1$0.t0);
  20. return context$1$0.abrupt("return", false);
  21. case 12:
  22. case "end":
  23. return context$1$0.stop();
  24. }
  25. }, foo, this, [[0, 8]]);
  26. });

这和我们的手动推导有明显的相似性,比如switch/case语句,而且我们甚至可以看到,val被拉到了闭包外面,正如我们做的那样。

当然,一个代价是这个generator的转译需要一个帮助工具库regeneratorRuntime,它持有全部管理一个普通generator/迭代器 所需的可复用逻辑。它的许多模板代码看起来和我们的版本不同,但即便如此,概念还是可以看到的,比如使用context$1$0.next = 4追踪generator的下一个状态。

主要的结论是,generator不仅限于ES6+的环境中才有用。一旦你理解了它的概念,你可以在你的所有代码中利用他们,并使用工具将代码变形为旧环境兼容的。

这比使用PromiseAPI的填补来实现前ES6的Promise要做更多的工作,但是努力完全是值得的,因为对于以一种可推理的,合理的,看似同步的顺序风格来表达异步流程控制来说,generator实在是好太多了。

一旦你适应了generator,你将永远不会回到面条般的回调地狱了!