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

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

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

    考虑这段代码:

    第一个要注意的事情是,我们仍然需要一个可以被调用的普通的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不同的部分:

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

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

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

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

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

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

    在状态*1*,与yield request(..)相反,我们return request(..)。在终结状态*2*,没有明确的,所以我们仅仅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. done: true,
    36. value: process( v )
    37. };
    38. }
    39. // generator 已经完成了
    40. else {
    41. return {
    42. done: true,
    43. value: undefined
    44. }
    45. },
    46. "throw": function(e) {
    47. // 在状态 *1* 中,有唯一明确的错误处理
    48. if (state == 1) {
    49. state = 3;
    50. return {
    51. done: true,
    52. value: process( e )
    53. };
    54. }
    55. // 否则,是一个不会被处理的错误,所以我们仅仅把它扔回去
    56. else {
    57. throw e;
    58. }
    59. }
    60. };
    61. }

    这段代码如何工作?

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

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

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

    自动转译

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

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

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

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

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

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

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

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