babel源码浅读

537 阅读6分钟

标签(空格分隔): 前端


[TOC]

参考文章 juejin.cn/post/684490… github.com/hoperyy/wat…

简述

babel是什么

babel是一个接受输入,进行处理,进行输出的工具。 用途是将输入的高版本的js代码转为兼容旧版本的代码。 使用babel本质上是对字符串进行处理,插件越多,进行的处理越多,也就会越耗时。所以从优化上来说的话,应当懂得自己的目标需求,将使用的插件减少到必要范围内。

使用方式

最常使用的是结合 webpack 一起使用的 babel-loader,另外 babel-cli 提供了命令行执行 npx babel xxx --xxxoptions的方式。

关于插件

plugin 配置的是具体使用的插件。 preset 因为有些插件经常结合使用,所以就把一组插件的集合设置一个 preset。最终 preset 在被使用时还是会被变成一个个 plugin 放到 visitor 对象中。

插件分为语法插件和转译插件,前者只是负责开启 babel-parser 本身具备的能力,后者开发时按照约定的姿势写 visitor。

如果不开启任何插件,bebel 执行后将原样出处,但是中间依然经理 ast 处理过程,会耗费性能。

关于 @babel/runtime

www.babeljs.cn/docs/babel-… 只是把一些反复使用的函数抽取出来,放进模块里,这样在生成的代码中不用反复写同样的代码。

简要流程

  • 从 babel-cli 或 babel-loader 得到原始的文件内容或原始字符串
  • _parser.default() 中处理参数中的 preset plugin,得到将要使用的 plugin 列表。
  • @babel/parser 中运行 _parser.parse(input),这里会进行 new Node() 递归进行词法解析和语法解析生成 ast 节点。【对原始字符串用 str.codePointAt() 得到原始字符的 unicode,然后使用一定规则判断该字符的意义,得到原始 ast
  • traverse() 会递归调用 traverse.node() 遍历原始 ast配合 plugin 得到目标 ast,babel本身不做什么。
  • @babel/generator 结合 printer 递归处理目标 ast,并且在输出上利用了 buffer, 用_flush 递归目标 ast,去进行 this._append, 利用 this._buf.join("").trimRight() 得到目标字符串。【关于buffer.concat】
  • 输出目标字符串。

源码小trick

详细流程

关于 _interopRequireDefault

是为了兼容 commonjs和es6的模块。 stackoverflow.com/questions/5… function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

关于 commonjs中的 module.exports 和 exports

两者最初都是指向同一个对象,exports.a = 1的话,module.exports上面也会有1,但是exports本身就是一个变量而已,如果 module.exports = {},那么输出的就会是新的对象,而 exports指向的还是老的对象。那整个模块输出的就会是新的对象,而不是exports指向的对象了。但是设置 exports = xxx 就不行了,只是把 exports 指向了别处,而输出的是 module.exports。

关于 babel-cli 入口

  • @babel/cli index.js 中直接 throw new Error("Use the @babel/core package instead of @babel/cli.");
  • package.jsonbin 定义的 babel 命令,指向 bin/babel.js
  • 如果没有传入其他参数决定使用什么 plugin 或 preset 的话,就原样输出读入的文件内容。但是 npx babel 18-jianzhi.js --presets=@babel/preset-env 就能输出转换后的代码。

以 babel-cli 为入口开始执行

  • 从命令行或npx babel 1.js 会在babel/cli中先读取options,经过options中的函数的处理,得到最终的参数,然后决定使用 dir 函数还是 file 函数。这里测试是使用的是 file。
  • /Users/liujunyang/work/xuetang/gets3/node_modules/@babel/core/lib/transform-file.js 中,设置的 @babel/preset-env 在 babel-core 中经过它的 config.default 函数处理后,都变成了 plugin 数组中的元素(目前 40 个),而 preset数组成了空数组。
  • 然后通过 fs.readFile 读取目标文件,然后运行 _transformation.run 进行处理,
  • _transformation 模块中,先执行 _normalizeFile,在 _normalizeFile 中,判断如果没有 ast 的话,就先调用 _parser.default(pluginPasses【plugin列表】, options【文件名、是不是babelrc方式,cwd等】, code【输入的解析前的代码】) 去生成 ast,【ast 根部的 type 必须是 Program 或 File】,
  • 上面在 _parser.default 中会逐个判断 plugin 有没有设置 parserOverride, 然后运行 _parser().parse, 也就是进入了 @babel/parser【是用TS 2.9写的】,
  • @babel/parser 中代表输入的代码的变量名是叫 input, 这里会进行 new Node() 去生成节点【一个 Node 实例有 type start end loc【SourceLocation】 range这些属性】,
  • 接下来会 getTokenFromCode 直到 finishToken
  • getTokenFromCode 用的就是 str.codePointAt() 得到每一个字符的unicode值【如let 的 l 就是 108】,然后逐步去 readWord(), 就是读取一个个的字符或者词语,比如把 let arr = [ 11 , 等字符给识别出来,
  • 最终 parse() 方法返回出来的变量名叫 file也就是传说中的 ast
  • 得到的结果类似下面的结构,比如 type: VariableDeclaration 用了关键词 kind: let 来表示是怎么进行变量声明的,type: VariableDeclarator 用的 name: arr 来表示变量名,type: NumericLiteral 用的 value: 12 来表示的元素的值。总之不同的 type 用了不同的字段去表述。共同的结构比如都有 start end loc 等等【具体结构见文末】。
  • 接下来回到 @babel/core 中 开始 transformFile 进行转换。这个函数中直接执行了 for 循环【数组只有一个值,内容是一个数组】
  • 然后执行了 _traverse().default.visitors.merge 得到了 visitor 对象,每个键名就是一个插件的名字,值就是插件的 enter 方法等组成的对象。
  • 然后执行 (0, _traverse().default)(file.ast, visitor, file.scope);ast 进行遍历,然后这些 visitor【其实就是一个个的babel插件】 去操作每一步遍历过程的 ast 节点。
  • traverse() 会递归调用 traverse.node(), 其中关键的就是 _context.default() 方法生成了一个 context 对象,
  • 然后调用了 context.visit()方法,然后其中根据是不是数组分别调用 visitMultiple visitSingle 方法。
  • 在这个 traverse.node() 的过程中,把 ast 转换成了目标 ast【比如 kind: let 变成了 kind: var】TODO。
  • 然后继续在 @babel/core 中的 transformFile 调用 _generate.default 方法去生成目标代码。就进入了 @babel/generator
  • @babel/generator 利用了 buffer, 用_flush 递归目标 ast,去进行 this._append, 利用 this._buf.join("").trimRight() 得到目标字符串然后返回出去。

原始 ast 简要结构

 {
	"type": "File",
	"start": 0,
	"end": 32,
	"loc": {},
	"errors": [],
  "comments": [],
	"program": {
    "type": "Program",
    "start": 0,
    "end": 32,
    "loc": {},
    "sourceType": "module",
    "interpreter": null,
    "body": [
      {
        "type": "VariableDeclaration",
        "kind": "let",
        "start": 0,
        "end": 31,
        "loc": {},
        "declarations": [
          {
            "type": "VariableDeclarator",
            "name": "arr",
            "start": 4,
            "end": 31,
            "loc": {},
            "id": {
              "type": "Identifier",
              "start": 4,
              "end": 7,
              "loc": {},
            },
            "init": {
              "type": "ArrayExpression",
              "start": 10,
              "end": 31,
              "loc": {},
              "elements": [
                {
                  "type": "NumericLiteral",
                  "start": 11,
                  "end": 12,
                  "loc": {},
                  "value": 2
                },
              ]
            }
          }
        ],
      }
    ],
  },
}