Babel的奇妙冒险

1,059 阅读5分钟

Babel的奇妙冒险

之前想找 Babel 相关的原理和代码解析的文章,一直未能如愿,所以决定自己翻一下源码看看。

由于代码结构比较复杂,直接看比较蒙,所以选择从第一个提交开始看, 代码切换到核心提交,看下代码的变迁。

git clone git@github.com:babel/babel.git

起点

git reset --hard aedcd4e12fd2dca3b415b0019fb96962a8bad27d

最初的提交只有一个文件 acorn.js, babel 项目最初应该是 fork 自acorn

acorn.js 是一个JavaScript实现的代码解析器,入口为parse方法,通过核心方法readTokenparseStatement交替执行完成解析。

readToken方法通过各种分支判断会路由到readWordreadNumber等分支处理,处理结束后会通过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.react transformer.
  • 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 产生影响