Babel的奇妙冒险
之前想找 Babel 相关的原理和代码解析的文章,一直未能如愿,所以决定自己翻一下源码看看。
由于代码结构比较复杂,直接看比较蒙,所以选择从第一个提交开始看, 代码切换到核心提交,看下代码的变迁。
git clone git@github.com:babel/babel.git
起点
git reset --hard aedcd4e12fd2dca3b415b0019fb96962a8bad27d
最初的提交只有一个文件 acorn.js, babel 项目最初应该是 fork 自acorn。
acorn.js 是一个JavaScript实现的代码解析器,入口为parse方法,通过核心方法readToken和parseStatement交替执行完成解析。
readToken方法通过各种分支判断会路由到readWord,readNumber等分支处理,处理结束后会通过finishToken记录当前tok信息,parseStatement通过各种条件分支,会路由到parseFunciton, parseNode等分支, 结束后会通过finishNode记录当前Node信息。
整体上 parse 方法只需要遍历一次就能完成解析,并生成AST语法树。
第一次提交代码很有意思,值得一看。
性能对比测试
git reset --hard a1d958751911faa06a18d0f99d5ca98d053ce655
acorn 引入 esprima 和 UglifyJS 做解析速度对比测试。
acorn的终点
git reset --hard daedc6fcb323cd6e205a87d1d8e5b78d1fbca090
这里是项目中 acorn 代码的最后一次提交,babel 最初应该是从这个提交 fork 的 acorn 项目。
新的开始 6to5 [ Sebastian McKenzie ](2014-09-28)
git reset --hard c97696c224d718d96848df9e1577f337b45464be
babel 项目最初的名为 6to5, 此次提交的message为'first commit', 提交时间为2014-09-28,fork 于 acorn 的代码经过一番研究被完全抛弃了。
项目结构整体比较简单,核心代码都在 /lib/6to5 下,主要使用 esprima 生成AST树,使用 estraverse 遍历语法树进行节点修改,通过 escodegen 生成 Javascript 代码。
// package.json
"escodegen": "https://github.com/Constellation/escodegen/tarball/master",
"esprima": "https://github.com/esnext/esprima/tarball/harmony-esnext",
"estraverse": "^1.5.1",
一个小的demo
下面是一个简单的把箭头函数转为 function 声明的简单实例。
var esprima = require("esprima");
var estraverse = require("estraverse");
var escodegen = require("escodegen");
var raw = " \ const greet = (name) => { \
return 'hello' + name; \
}; \
";
var ast = esprima.parse(raw);
estraverse.traverse(ast, {
enter: function (node, parent) {
if (node.type === 'VariableDeclaration') {
node.kind = 'var'
}
if (node.type === 'ArrowFunctionExpression') {
node.type = 'FunctionExpression'
}
}
});
var code = escodegen.generate(ast);
console.log(code);
// 结果
var greet = function (name) {
return 'hello' + name;
};
源码片段解析
源码中 body.type !== "BlockStatement" 处理箭头函数没有用 {} 如 () => return 1 的情况。
后续又遍历了函数体,如果包含 this 表达式,会用模板方式为函数加上 bind(this)
exports.ArrowFunctionExpression = function (node) {
var body = node.body;
if (body.type !== "BlockStatement") {
body = {
type: "BlockStatement",
body: [{
type: "ReturnStatement",
argument: body
}]
};
}
node.expression = false;
node.body = body;
node.type = "FunctionExpression";
if (traverse.hasType(node, "ThisExpression")) {
return util.template("function-bind-this", {
FUNCTION: node
});
} else {
return node;
}
};
其他
- MIT License
- 平时前端项目基本没有用过 Makefile, 此时项目的 Makefile 内容还很简单,适合学习了解下
- 持续集成 travis
- editorconfig 配合编辑器插件做格式化很方便
- istanbul, mocha 做代码测试
测试代码思路大致是:遍历 test/fixtures 下的每一个文件夹(suiteName), 再遍历每个 suite 下的文件夹(taskName), 根据 task 下的actual.js 生成代码和expected.js做对比。
查看 UT 代码也是快速了解项目实现功能的手段
v1.7.7
这是目前 github 上保留的最早的tag
v1.10.7 (一个月后 2014-10-28)
// package.json
"es6-shim": "0.18.0",
"es6-symbol": "0.1.1",
"acorn-jsx": "https://github.com/sebmck/acorn-jsx/archive/master.tar.gz",
"acorn-recast": "0.8.0-4",
"acorn-ast-types": "0.5.3-1"
整体变化不大,解析换成了acorn-recast, 不过一些包 install 时已经找不到了。值得注意的是,README 中已经看到了一些 Polyfill 的简单说明
Babel的诞生
追踪后续变化
后续主要变化可以 CHANGELOG.md 查看, v2.6.0 CHANGELOG.md 添加tags。
[New Feature]
[Bug Fix]
[Spec Compliancy]
[Breaking Change]
[Documentation]
[Internal]
[Polish]
v4.0.0 改名为babel
v4.0.1 之后,CHANGELOG.md 之追踪v4 之后的变更,v4之前的在CHANGELOG-6to5.md记录
V7.0.0 之前的一些变更
- 6.1.0 Add babel-doctor CLI.
- 6.0.0 Split up internals into packages. 代码结构发生较大调整,基本与现在代码一致
- 5.8.21 Add support for Flow export types.
- 5.7.0
- Add .babelignore file to be consistent with other tools.
- Allow .babelrc configs to be specified via package.json.
- 5.4.0 Added env option. Especially handy when using the .babelrc
- 5.1.0 Add trailing function comma proposal.
- 5.0.0 新增较多 Feature 的一个大版本
- Allow ES7 transformer to be enabled via optional instead of only via stage
- Add support for .babelrc on absolute paths.
- Plugin API
- 4.5.1 Babel now compiles itself! (哈哈哈哈哈)
- 4.5.0 Add .babelrc support.
- 4.0.0 6to5 is now known as Babel
- 3.6.5 Add
validation.reacttransformer. - 3.6.4 Add support for flow type casts and module types
- 3.4.0 Add commonStandard module formatter.
小结
整体上看来,最初的acorn AST语法树代码; 6to5 first commit; v4.0.0 babel 成型的代码; v6.0.0结构重大调整; 以上4个是适合阅读了解babel项目的重要节点。
Babel 代码梳理
- @babel/core
- @babel/parser 生成AST语法树
- @babel/traverse 遍历维护整个树的状态,并负责替换、删除和添加节点
- @babel/generator 将AST语法树转为Javascript代码
- @babel/cli 命令行工具,命令行运行@babel/core
- @babel/types 校验修改AST节点
babel-polyfill
- 用途
- 包含 regenerator 和 core-js,模拟一个完全的 ES2015+ 的环境
- 通过改写全局prototype的方式实现,针对编译的代码中新的API进行处理,并且在代码中插入一些帮助函数
- 解决了Babel不转换新API的问题
- 问题
- 污染了全局环境
- 不同的代码文件中包含重复的代码,导致编译后的代码体积变大
babel-runtime
- babel-runtime用以提供编译模块的工具函数
- 启用插件babel-plugin-transform-runtime后,Babel就会使用babel-runtime下的工具函数
- babel-runtime插件能够将这些工具函数的代码转换成require语句,指向为对babel-runtime的引用。每当要转译一个api时都要手动加上require('babel-runtime')
- 按需加载
例如
require Promise from 'babel-runtime/core-js/promise
babel-plugin-transform-runtime
- 解决使用 babel-runtime,解决手动 require 问题。
- 分析的 ast 是否有引用 babel-rumtime 中的垫片(通过映射关系),在当前模块顶部插入需要的垫片。
- transform-runtime 不会污染原生的对象,方法,也不会对其他 polyfill 产生影响