2022-10-7 About 7 min

本章将详细介绍 Babel 对 async / generator 的转译。

# 12.1 前置知识

# Generator 对象

Generator 指的是生成器对象,该对象由 generator 函数返回,Generator 对象符合迭代协议。

function* gen() { // generator 函数
    yield 'a';
    yield 'b';
}

let g = gen(); // "Generaotr 对象 {}"

console.log(g.next()); // { value: "a", done: false }
console.log(g.next()); // { value: "b", done: false }
console.log(g.next()); // { value: undefined, done: true }
1
2
3
4
5
6
7
8
9
10

# 迭代协议

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols#iterable

作为 ECMAScript 2015 的一组补充规范,迭代协议并不是新的内置实现或语法,而是协议。这些协议可以被任何遵循某些约定的对象来实现。

迭代协议具体分为两个协议: 可迭代协议和迭代器协议。

# 可迭代协议

可迭代协议允许 JavaScript 对象设置自身的迭代行为。比如在 for...of 结构中,对象的哪些值可以被遍历到。

要成为可迭代对象,一个对象必须实现 @@iterator 方法。这意味着对象(或者其原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性,示例:

const obj = {
    [Symbol.iterator]: function() { return this }
}
1
2
3
  • 内置可迭代协议的对象

    目前所有的内置可迭代对象如下: StringArrayTypedArrayMapSet,它们的原型对象都实现了 @@iterator 方法。

    比如 String 关于 @@iterator 方法的示例:

    const someString = "hi";
    typeof someString[Symbol.iterator]; // "function"
    
    const iterator = someString[Symbol.iterator]();
    iterator + "";    // "[object String Iterator]"
    
    iterator.next();  // { value: "h", done: false }
    iterator.next();  // { value: "i", done: false }
    iterator.next();  // { value: undefined, done: true }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    另外,展开语法(...)内部也实现了可迭代协议:

    [...someString] // ["h", "i"]
    
    1
  • 自定义可迭代对象

    对象也可以通过提供自己的 @@iterator 方法,重新定义迭代行为:

    // 必须构造 String 对象以避免字符串字面量 auto-boxing
    const someString = new String("hi");
    someString[Symbol.iterator] = function() {
        return { // 只返回一次元素,字符串 "bye",的迭代器对象
            next: function() {
                if (this._first) {
                    this._first = false;
                    return { value: "bye", done: false };
                } else {
                    return { done: true };
                }
            },
            _first: true
        };
    };
    
    [...someString]; // ["bye"]
    someString + ""; // "hi"
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const myIterable = {};
    myIterable[Symbol.iterator] = function* () {
        yield 1;
        yield 2;
        yield 3;
    };
    [...myIterable]; // [1, 2, 3]
    
    1
    2
    3
    4
    5
    6
    7
  • 接受可迭代对象的 API

    很多 API 接受可迭代对象作为参数,例如:

    new Map([iterable])
    new WeakMap([iterable])
    new Set([iterable])
    new WeakSet([iterable])
    Promise.all(iterable)
    Promise.race(iterable)
    Array.from(iterable)
    
    1
    2
    3
    4
    5
    6
    7
  • 需要可迭代对象的语法

    一些语句和表达式需要可迭代对象,比如 for...of 循环、展开语法、yield*,和解构赋值。

    for(let value of ["a", "b", "c"]){
        console.log(value);
    }
    // "a"
    // "b"
    // "c"
    
    [..."abc"]; // ["a", "b", "c"]
    
    function* gen() {
    yield* ["a", "b", "c"];
    }
    
    gen().next(); // { value: "a", done: false }
    
    [a, b, c] = new Set(["a", "b", "c"]);
    a // "a"
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

# 迭代器协议

  • 什么是迭代器协议

    在 JavaScript 中,迭代器是一个对象,它定义一个序列,并在序列终止时可能返回一个返回值。

    迭代器协议描述了一个迭代器需要长什么样子,或者说需要有如下功能:

    • 迭代器对象需实现了一个 next() 方法

    • next() 方法必须返回一个对象,该对象应当有两个属性: donevalue

    • donevalue 需要满足如下语义:

      • done: boolean

        如果迭代器可以产生序列中的下一个值,则为 false

        如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。

      • value: any

        迭代器返回的任何 JavaScript 值。donetrue 时可省略。

  • 迭代器示例

    简单迭代器

    function makeIterator(array) {
        let nextIndex = 0;
        return {
            next: function () {
                return nextIndex < array.length ? {
                    value: array[nextIndex++],
                    done: false
                } : {
                    done: true
                };
            }
        };
    }
    
    let it = makeIterator(['哟', '呀']);
    
    console.log(it.next().value); // '哟'
    console.log(it.next().value); // '呀'
    console.log(it.next().done);  // true
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    无穷迭代器

    function idMaker() {
        let index = 0;
        return {
        next: function() {
            return {
                value: index++,
                done: false
            };
        }
        };
    }
    
    let it = idMaker();
    
    console.log(it.next().value); // '0'
    console.log(it.next().value); // '1'
    console.log(it.next().value); // '2'
    // ...
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    使用生成器

    function* makeSimpleGenerator(array) {
        let nextIndex = 0;
    
        while(nextIndex < array.length) {
            yield array[nextIndex++];
        }
    }
    
    let gen = makeSimpleGenerator(['哟', '呀']);
    
    console.log(gen.next().value); // '哟'
    console.log(gen.next().value); // '呀'
    console.log(gen.next().done);  // true
    
    function* idMaker() {
        let index = 0;
        while (true) {
            yield index++;
        }
    }
    
    let gen = idMaker();
    
    console.log(gen.next().value); // '0'
    console.log(gen.next().value); // '1'
    console.log(gen.next().value); // '2'
    // ...
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

    ES2015 类 class 中的迭代器

    class SimpleClass {
        constructor(data) {
            this.data = data
        }
    
        [Symbol.iterator]() {
            // Use a new index for each iterator. This makes multiple
            // iterations over the iterable safe for non-trivial cases,
            // such as use of break or nested looping over the same iterable.
            let index = 0;
    
            return {
                next: () => {
                    if (index < this.data.length) {
                        return {value: this.data[index++], done: false}
                    } else {
                        return {done: true}
                    }
                }
            }
        }
    }
    
    const simple = new SimpleClass([1,2,3,4,5])
    
    for (const val of simple) {
        console.log(val)   //'1' '2' '3' '4' '5'
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

# Generator 对象到底是一个迭代器,还是一个可迭代对象?

回顾下,迭代器指的是实现了 [Symbole.iterator] 属性的对象,可迭代对象是实现了 next() 方法且其返回值是符合规范的 { done, value } 类型的对象。

生成器对象既是迭代器,也是可迭代对象:

let aGeneratorObject = function* (){
    yield 1;
    yield 2;
    yield 3;
}();

typeof aGeneratorObject.next;
// 返回 "function", 因为有一个 next 方法,所以这是一个迭代器

typeof aGeneratorObject[Symbol.iterator];
// 返回 "function", 因为有一个 @@iterator 方法,所以这是一个可迭代对象

aGeneratorObject[Symbol.iterator]() === aGeneratorObject;
// 返回 true, 因为 @@iterator 方法返回自身(即迭代器),所以这是一个格式良好的可迭代对象

[...aGeneratorObject];
// 返回 [1, 2, 3]

console.log(Symbol.iterator in aGeneratorObject)
// 返回 true, 因为 @@iterator 方法是 aGeneratorObject 的一个属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 12.2 Babel 对 generator 函数的转译

# 转译过程概述

并不是所有运行环境都支持 generator 语法的,对于这些环境,Babel 可以将其转译为 ES5 版本,其中,babel-preset-env 和 babel-plugin-transform-runtime 均与 generator 函数转译相关。

例如,待转译的源码为:

function* gen() {
    yield 'a';
    yield 'b';
}
1
2
3
4

在不同的 Babel 配置下,有不同的转译结果。

  • 只配置 babel-preset-env

    Babel 转译配置如下:

    {
        "presets": [
            [ "@babel/preset-env" ]
        ]
    }
    
    1
    2
    3
    4
    5

    源码会被转译为:

    "use strict";
    
    var _marked = /*#__PURE__*/regeneratorRuntime.mark(gen);
    
    function gen() {
        return regeneratorRuntime.wrap(function gen$(_context) {
            while (1) {
                switch (_context.prev = _context.next) {
                    case 0:
                    _context.next = 2;
                    return 'a';
    
                    case 2:
                    _context.next = 4;
                    return 'b';
    
                    case 4:
                    case "end":
                    return _context.stop();
                }
            }
        }, _marked);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    转译结果依赖了全局变量 regeneratorRuntime

  • 新增 babel-plugin-transform-runtime

    如果 Babel 的转译配置增加 babel-plugin-transform-runtime 插件:

    {
        "plugins": [
            [ "@babel/plugin-transform-runtime" ]
        ],
        "presets": [
            [ "@babel/preset-env" ]
        ]
    }
    
    1
    2
    3
    4
    5
    6
    7
    8

    源码会被转译为:

    "use strict";
    
    var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
    
    var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
    
    var _marked = /*#__PURE__*/_regenerator.default.mark(gen);
    
    function gen() {
        return _regenerator.default.wrap(function gen$(_context) {
            while (1) {
                switch (_context.prev = _context.next) {
                    case 0:
                    _context.next = 2;
                    return 'a';
    
                    case 2:
                    _context.next = 4;
                    return 'b';
    
                    case 4:
                    case "end":
                    return _context.stop();
                }
            }
        }, _marked);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

    转译结果提供了 _regenerator 对象,避免依赖全局的 regeneratorRuntime

# regenerator-runtime

资料: https://segmentfault.com/a/1190000023701257

对于源码:

function* gen() {
    yield 'a';
    yield 'b';
}
1
2
3
4

在只配置 babel-preset-env 时,转译结果是:

"use strict";

var _marked = /*#__PURE__*/regeneratorRuntime.mark(gen);

function gen() {
    return regeneratorRuntime.wrap(function gen$(_context) {
        while (1) {
            switch (_context.prev = _context.next) {
                case 0:
                _context.next = 2;
                return 'a';

                case 2:
                _context.next = 4;
                return 'b';

                case 4:
                case "end":
                return _context.stop();
            }
        }
    }, _marked);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

单独看转译结果,有 2 个问题:

  • 这个转译结果并不能独立运行起来,它依赖了一个全局变量 regeneratorRuntime
  • 转译后代码的运行逻辑是怎样的,比如 regeneratorRuntime.wrapregeneratorRuntime.mark 都是什么意思

首先,需确认下,转译后代码中的 regeneratorRuntime 来自 babel-runtime 包,babel-runtime 包又依赖了 regenerator-runtime,即依赖关系是:

babel-preset-env -> babel-runtime -> regenerator-runtime
1

regenerator-runtime 工具包是 Facebook 公司推出的用 ES5 写法运行 ES6 的 generator 函数的辅助工具,其目录主要结构为:

regenerator
├─ README.md
├─ bin
├─ main.js
├─ packages
│    ├─ preset
│    ├─ runtime
│    └─ transform
1
2
3
4
5
6
7
8

regeneratorRuntime 定义在文件 regenerator/packages/runtime/runtime.js,具体的,regeneratorRuntime 有如下功能:

  • regeneratorRuntime 挂载到全局
  • 定义 regeneratorRuntime.mark
  • 定义 regeneratorRuntime.wrap

源码有 750 行左右,通读该源码可以了解 regeneratorRuntime 的作用。

笔者整理了一份精简版的 regeneratorRuntime 定义,可简要地介绍其核心逻辑。

(function () {
    var window = self = global;
    var ContinueSentinel = {};

    var mark = function (genFun) {
        var generator = Object.create({
            next: function (arg) {
                return this._invoke("next", arg);
            }
        });
        genFun.prototype = generator;
        return genFun;
    };

    function wrap(innerFn, outerFn, self) {
        var generator = Object.create(outerFn.prototype);

        var context = {
            done: false,
            method: "next",
            next: 0,
            prev: 0,
            abrupt: function (type, arg) {
                var record = {};
                record.type = type;
                record.arg = arg;

                return this.complete(record);
            },
            complete: function (record, afterLoc) {
                if (record.type === "return") {
                    this.rval = this.arg = record.arg;
                    this.method = "return";
                    this.next = "end";
                }

                return ContinueSentinel;
            },
            stop: function () {
                this.done = true;
                return this.rval;
            }
        };

        generator._invoke = makeInvokeMethod(innerFn, context);

        return generator;
    }

    function makeInvokeMethod(innerFn, context) {
        var state = "start";

        return function invoke(method, arg) {
            if (state === "completed") {
                return { value: undefined, done: true };
            }

            context.method = method;
            context.arg = arg;

            while (true) {
                state = "executing";

                var record = {
                    type: "normal",
                    arg: innerFn.call(self, context)
                };

                if (record.type === "normal") {
                    state = context.done ? "completed" : "yield";

                    if (record.arg === ContinueSentinel) {
                        continue;
                    }

                    return {
                        value: record.arg,
                        done: context.done
                    };
                }
            }
        };
    }

    window.regeneratorRuntime = {};

    regeneratorRuntime.wrap = wrap;
    regeneratorRuntime.mark = mark;
})();

var _marked = regeneratorRuntime.mark(helloWorldGenerator);

function gen() {
    return regeneratorRuntime.wrap(
        function gen$(_context) {
            while (1) {
                switch (_context.prev = _context.next) {
                    case 0:
                    _context.next = 2;
                    return 'a';

                    case 2:
                    _context.next = 4;
                    return 'b';

                    case 4:
                    case "end":
                    return _context.stop();
                }
            }
        },
        _marked,
        this
    );
}

// 运行 gen
var g = gen();
g.next();
g.next();
g.next();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121

其运行图如下:

Runtime/regenerator-runtime

Last update: October 7, 2022 19:03
Contributors: hoperyy