babel源码详解-v1.7.8

继续打开 github 看一下最初的版本的 babel 是怎么实现的,了解它的基本原理。

git clone git@github.com:babel/babel.git 并且 git checkout v1.7.7npm i 安装一下相应的 node 包。其实还可以找到更早的 tag ,但由于之前的一些依赖包现在已经下载不下来了,程序跑不起来不好调试所以就没用了。

看一下 package.json

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
{
"name": "6to5",
"description": "Turn ES6 code into vanilla ES5 with source maps and no runtime",
"version": "1.7.7",
"author": "Sebastian McKenzie <sebmck@gmail.com>",
"homepage": "https://github.com/sebmck/6to5",
"repository": {
"type": "git",
"url": "https://github.com/sebmck/6to5.git"
},
"bugs": {
"url": "https://github.com/sebmck/6to5/issues"
},
"preferGlobal": true,
"main": "lib/6to5/node.js",
"bin": {
"6to5": "./bin/6to5",
"6to5-node": "./bin/6to5-node"
},
"keywords": [
"es6-transpiler",
"scope",
"harmony",
"blockscope",
"block-scope",
"let",
"const",
"var",
"es6",
"transpile",
"transpiler",
"traceur",
"6to5"
],
"scripts": {
"bench": "make bench",
"test": "make test"
},
"dependencies": {
"ast-types": "0.5.0",
"commander": "2.3.0",
"fs-readdir-recursive": "0.0.2",
"lodash": "2.4.1",
"mkdirp": "0.5.0",
"es6-shim": "0.18.0",
"es6-symbol": "0.1.1",
"regexpu": "0.2.2",
"recast": "0.8.0",
"source-map": "0.1.40"
},
"devDependencies": {
"es6-transpiler": "0.7.17",
"istanbul": "0.3.2",
"matcha": "0.5.0",
"mocha": "1.21.4",
"traceur": "0.0.66",
"esnext": "0.11.1",
"es6now": "0.8.11",
"jstransform": "6.3.2",
"uglify-js": "2.4.15",
"browserify": "6.0.3",
"proclaim": "2.0.0"
}
}

当时的名字还叫 6to5 ,依赖的包很多,就不能像 eslint-v0.0.2做了什么 那样一个一个包讲了,这里只记录一下主流程依赖的一些包。

运行调试

我们可以写一个简单的 input.js 然后试一下。

1
2
// input.js
const data = "test";

执行一下 ./bin/6to5 -h 看一下帮助。

1
2
3
4
5
6
7
8
9
10
11
12
Usage: 6to5 [options] <files ...>

Options:

-h, --help output usage information
-t, --source-maps-inline Append sourceMappingURL comment to bottom of code
-s, --source-maps Save source map alongside the compiled code when using --out-file and --out-dir flags
-w, --whitelist [whitelist] Whitelist
-b, --blacklist [blacklist] Blacklist
-o, --out-file [out] Compile all input files into a single file
-d, --out-dir [out] Compile an input directory of modules into an output directory
-V, --version output the version number

-o 是指定输出的文件,测试一下,./bin/6to5 -o output.js input.js 。然后就得到了 output.js

1
2
3
4
//output.js
(function() {
var data = "a";
})();

帮我们把 const 换成了 var,同时通过自执行函数包了一层作用域。

Vscode 新建一个 launch.json ,选择 Node.js

把默认生成的 program 字段去掉,加上 args

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "debug Program",
"skipFiles": ["<node_internals>/**"],
"runtimeExecutable": "node",
"args": ["./bin/6to5", "-o", "output.js", "input.js"]
}
]
}

添加相应的断点,然后 F5 就可以愉快的调试了。

命令行框架用的是 commandergithub 有超详细的使用方法,这里就不再说了,下边介绍 babel 相关的主要原理。

主要原理

通过不断的运行调试,渐渐了解了主流程,但直到看到尤大推荐的这个 mini 编译器才对整个框架有了更深的了解。

强烈推荐先过去 看一下,对 babel 可以有一个更直接的了解。

babel 本质上还是对 AST 的操控,可以认为是一个编译器了,只不过是 jsjs 的转换。

一个编译器主要是三个步骤,解析(词法分析、语法分析)-> 转换 -> 生成目标代码。

第一步「解析」就是去生成一个 AST,主要分两步。

  • 词法分析,分词

    1
    2
    3
    4
    5
    6
    7
    对于 const data = "test"; 经过分词就是下边的结果
    [
    { type: 'Keyword', value: 'const' },
    { type: 'Identifier', value: 'data' },
    { type: 'Punctuator', value: '=' },
    { type: 'String', value: '"test"' }
    ]
  • 语法分析,生成抽象语法树(AST)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    通过上边分词,然后就可以生成一个 AST 树
    {
    "type": "Program",
    "body": [
    {
    "type": "VariableDeclaration",
    "declarations": [
    {
    "type": "VariableDeclarator",
    "id": {
    "type": "Identifier",
    "name": "data"
    },
    "init": {
    "type": "Literal",
    "value": "test",
    "raw": "\"test\""
    }
    }
    ],
    "kind": "const"
    }
    ]
    }

第二步「转换」就是基于上边的 AST 再进行增删改,或者基于它生成一个新的 AST

第三步「生成目标代码」就是基于新的 AST 来构建新的代码即可。

对于 Babel 的话,第一步是直接使用了 recast 包的 parse 方法,传入源码可以直接帮我们返回一个 AST 树。

第三步也可以直接使用 recast 包的 print 方法,传入 AST 树返回源码。

所以 babel 的核心就在于第二步,通过遍历旧的 AST 树来生成一个新的 AST 树。

遍历

核心方法就是 lib/6to5/traverse/index.js 中的 traverse 方法了,比较典型的深度优先遍历,遍历过程中根据传入的 callbacks 来更改 node 节点。

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
var traverse = module.exports = function (parent, callbacks, blacklistTypes) {
if (!parent) return;

// 当前节点是数组,分别遍历进入递归
if (_.isArray(parent)) {
_.each(parent, function (node) {
traverse(node, callbacks, blacklistTypes);
});
return;
}

// 拿到当前节点的 key 值,后边还会提到
var keys = VISITOR_KEYS[parent.type] || [];
blacklistTypes = blacklistTypes || [];

// 为了统一,如果传进来的 callbacks 是函数,将其转换为对象,后边还会提到
if (_.isFunction(callbacks)) {
callbacks = { enter: callbacks };
}

// 遍历当前节点的每一个 key
_.each(keys, function (key) {
var nodes = parent[key];
if (!nodes) return;

...

// 如果当前节点是数组就分别处理
if (_.isArray(nodes)) {
_.each(nodes, function (node, i) {
handle(nodes, i);
});

// remove deleted nodes
parent[key] = _.flatten(parent[key]).filter(function (node) {
return node !== traverse.Delete;
});
} else {
handle(parent, key);

if (parent[key] === traverse.Delete) {
throw new Error("trying to delete property " + key + " from " +
parent.type + " but can't because it's required");
}
}
});
};

VISITOR_KEYS 其实就是枚举了所有的要处理的 node 节点的 key 值。

比如上边举的 const data = "test"; 的例子,它对应的 node 节点就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "data"
},
"init": {
"type": "Literal",
"value": "test",
"raw": "\"test\""
}
}
],
"kind": "const"
}

我们所要遍历的就是「包含 type 的对象」,比如上边的

1
2
3
4
5
6
7
8
9
10
11
12
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "data"
},
"init": {
"type": "Literal",
"value": "test",
"raw": "\"test\""
}
}

所以对于 VariableDeclaration 节点,它可以枚举的 key 就是 ['declarations'],它包含了 VariableDeclarator 节点。

同理,对于 VariableDeclarator 节点,它可以枚举的 key 就是 ['id', 'init']

VISITOR_KEYS 就是一个大对象,key 就是 node 节点的 typevalue 就是可以通过枚举得到 node 节点的所有 key

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
{
"ArrayExpression": ["elements"],
"ArrayPattern": ["elements"],
"ArrowFunctionExpression": ["params", "defaults", "rest", "body"],
"AssignmentExpression": ["left", "right"],
"AwaitExpression": ["argument"],
"BinaryExpression": ["left", "right"],
"BlockStatement": ["body"],
"BreakStatement": ["label"],
"CallExpression": ["callee", "arguments"],
"CatchClause": ["param", "body"],
"ClassBody": ["body"],
"ClassDeclaration": ["id", "body", "superClass"],
"ClassExpression": ["id", "body", "superClass"],
"ClassProperty": ["key", "value"],
"ComprehensionBlock": ["left", "right", "body"],
"ComprehensionExpression": ["filter", "blocks", "body"],
"ConditionalExpression": ["test", "consequent", "alternate"],
"ContinueStatement": ["label"],
"DebuggerStatement": [],
"DoWhileStatement": ["body", "test"],
"EmptyStatement": [],
...
"VariableDeclaration": ["declarations"],
"VariableDeclarator": ["id", "init"],
"VoidTypeAnnotation": [],
"WhileStatement": ["test", "body"],
"WithStatement": ["object", "body"],
"YieldExpression": ["argument"]
}

遍历过程中对于每个 node 节点都会执行 handle 函数,callback 是传入的回调函数,包含 enter 方法和 exit 方法。

1
2
3
4
{
enter: function(){},
exit: function(){},
}

enter 返回的节点替换当前节点,所有子节点遍历完成后再调用 exit 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var handle = function (obj, key) {
var node = obj[key];
if (!node) return;

// type is blacklisted
if (blacklistTypes.indexOf(node.type) >= 0) return;

// enter
var result = callbacks.enter(node, parent, obj, key);

// stop iteration
if (result === false) return;

// replace node
if (result != null) node = obj[key] = result;

traverse(node, callbacks, blacklistTypes);

// exit
if (callbacks.exit) callbacks.exit(node, parent, obj, key);
};

回调函数和模版

babel 定义了不同 transform 来作为回调函数,返回处理后的 node 节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
transformers
├── array-comprehension.js
├── arrow-functions.js
├── block-binding.js
├── classes.js
├── computed-property-names.js
├── constants.js
├── default-parameters.js
├── destructuring.js
├── for-of.js
├── generators.js
├── modules.js
├── property-method-assignment.js
├── property-name-shorthand.js
├── rest-parameters.js
├── spread.js
├── template-literals.js
└── unicode-regex.js

可以看一下 block-binding 的实现,主要作用就是在定义 var 变量的地方包一层自执行函数,也就是文章最开头写的测试例子。

1
2
3
4
//output.js
(function() {
var data = "a";
})();

block-binding.js 中的核心方法是 buildNode

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
var buildNode = function (node) {
var nodes = [];
...

// 包装所需要的 node 节点
var block = b.blockStatement([]);
block.body = node;

var func = b.functionExpression(null, [], block, false);

var templateName = "function-call";
if (traverse.hasType(node, "ThisExpression")) templateName += "-this";
if (traverse.hasType(node, "ReturnStatement", ["FunctionDeclaration", "FunctionExpression"])) templateName += "-return";

//

// 将模版中的节点替换为上边生成的节点
nodes.push(util.template(templateName, {
FUNCTION: func
}, true));

return {
node: nodes,
body: block
};
};

其中 bvar b = require("ast-types").builders; ,可以得到各种类型的 ast 节点。util.template 方法可以通过预先写的一些模版,将模版的某一块用传入的节点替换。

模版的话都写在了 templates 文件夹下。

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
templates
├── arguments-slice-assign-arg.js
├── arguments-slice-assign.js
├── arguments-slice.js
├── array-comprehension-container.js
├── array-comprehension-filter.js
├── array-comprehension-for-each.js
├── array-comprehension-map.js
├── array-concat.js
├── array-push.js
├── assign.js
├── class-inherits-properties.js
├── class-inherits-prototype.js
├── class-method.js
├── class-statement-container.js
├── class-static-method.js
├── class-super-constructor-call.js
├── class.js
├── exports-alias-var.js
├── exports-assign.js
├── exports-default-require-key.js
├── exports-default-require.js
├── exports-default.js
├── exports-require-assign-key.js
├── exports-require-assign.js
├── exports-wildcard.js
├── for-of.js
├── function-bind-this.js
├── function-call-return.js
├── function-call-this-return.js
├── function-call-this.js
├── function-call.js
├── function-return-obj-this.js
├── function-return-obj.js
├── if-undefined-set-to.js
├── if.js
├── obj-key-set.js
├── object-define-properties-closure.js
├── object-define-properties.js
├── prototype-identifier.js
├── require-assign-key.js
├── require-assign.js
├── require-key.js
├── require.js
├── variable-assign.js
└── variable-declare.js

看一下上边用到的 function-call 模版,function-call.js 文件里仅有一行,一个函数调用。

1
FUNCTION();

babel 预先会把上边 template 文件夹里的所有文件全部转成 ast 的语法树。

遍历 templates 下的所有文件。

1
2
3
4
5
6
7
8
9
10
// lib/6to5/util.js
_.each(fs.readdirSync(templatesLoc), function (name) {
var key = path.basename(name, path.extname(name));
var loc = templatesLoc + "/" + name;
var code = fs.readFileSync(loc, "utf8");

exports.templates[key] = exports.removeProperties(
exports.parse(loc, code).program
);
});

而上边使用的 exports.parse 就是调用了 recast 库的 parse 来返回 ast 树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
exports.parse = function (filename, code, callback) {
try {
var ast = recast.parse(code, {
sourceFileName: path.basename(filename),
});

if (callback) {
return callback(ast);
} else {
return ast;
}
}
...
};

再回到上边 block-binding.jsutil.template 方法来。

其中 bvar b = require("ast-types").builders; ,可以得到各种类型的 ast 节点。util.template 方法可以通过预先写的一些模版,将模版的某一块用传入的节点替换。

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
// nodes 传入我们需要替换的模版中的节点
exports.template = function (name, nodes, keepExpression) {
// 得到之前生成的模版 AST 树
var template = exports.templates[name];
if (!template) throw new ReferenceError("unknown template " + name);

template = _.cloneDeep(template);

if (!_.isEmpty(nodes)) {
// 遍历模版 AST 树
traverse(template, function (node) {
// 如果当前节点是我们需要替换的就进行替换
if (node.type === "Identifier" && _.has(nodes, node.name)) {
var newNode = nodes[node.name];
if (_.isString(newNode)) {
node.name = newNode;
} else {
return newNode;
}
}
});
}

var node = template.body[0];

if (!keepExpression && node.type === "ExpressionStatement") {
return node.expression;
} else {
return node;
}
};

总结

babel 编译器主要是三个步骤,解析(词法分析、语法分析)-> 转换 -> 生成目标代码,主要逻辑是第二步转换。

转换主要就是通过提前写好各种类型的 transform ,利用 traverse 方法遍历 AST 的所有 node 节点,遍历过程操作旧 node 节点来生成新的 node 节点(可以通过 recast 库辅助),再替换之前写好的模版的某一部分从而生成一个新的 AST

我感觉最复杂最细节的地方就是一个个的 transform 的编写了,需要对 AST 了解得非常清楚。

感觉文字不太好表述,大家可以按照最开始介绍的方法打断点然后结合上边的文字应该会更容易理解。

前端工程化其他系列文章大家感兴趣也可以看一下:

前端工程化发展历史

2021年从零开发前端项目指南

eslint 源码详解-v0.0.2

windliang wechat