【原创】解析 BaBel 转换 es6+的工作原理

1,501 阅读4分钟

babel 是什么?

Babel · The compiler for writing next generation Java

6to5

你在 npm 上可以看到这样一个包名字是 6to5(www.npmjs.com/package/6to… 光看名字可能会让人感觉到很诧异,名字看起来可能有点奇怪,其实 babel 在开始的时候名字就是这个。简单粗暴 es6->es5,一下子就看懂了 babel 是用来干啥的,但是很明显这不是一个好名字,这个名字会让人感觉到 es6 普及之后这个库就没用了,为了保持活力这个库可能要不停的修改名字。

babel 做了哪些事情

微信图片_20191215115442

为了转换我们的代码, babel 做了三件事:

①Parser 解析我们的代码转换为 AST。

②Transformer 利用我们配置好的 plugins/presets 把 Parser 生成的 AST 转变为 新的 AST。

③Generator 把转换后的 AST 生成新的代码

像我们在.babelrc 里配置的 presets 和 plugins 都是在第 2 步工作的。 举个例子,首先你输入的代码如下:

if (1 > 0) {
    alert('hi');
}

经过第1步得到一个如下的对象:


{
  "type": "Program",                          // 程序根节点
  "body": [                                   // 一个数组包含所有程序的顶层语句
    {
      "type": "IfStatement",                  // 一个if语句节点
      "test": {                               // if语句的判断条件
        "type": "BinaryExpression",           // 一个双元运算表达式节点
        "operator": ">",                      // 运算表达式的运算符
        "left": {                             // 运算符左侧值
          "type": "Literal",                  // 一个常量表达式
          "value": 1                          // 常量表达式的常量值
        },
        "right": {                            // 运算符右侧值
          "type": "Literal",
          "value": 0
        }
      },
      "consequent": {                         // if语句条件满足时的执行内容
        "type": "BlockStatement",             // 用{}包围的代码块
        "body": [                             // 代码块内的语句数组
          {
            "type": "ExpressionStatement",    // 一个表达式语句节点
            "expression": {
              "type": "CallExpression",       // 一个函数调用表达式节点
              "callee": {                     // 被调用者
                "type": "Identifier",         // 一个标识符表达式节点
                "name": "alert"
              },
              "arguments": [                  // 调用参数
                {
                  "type": "Literal",
                  "value": "hi"
                }
              ]
            }
          }
        ]
      },
      "alternative": null                     // if语句条件未满足时的执行内容
    }
  ]
}

Babel 实际生成的语法树还会包含更多复杂信息,这里只展示比较关键的部分 用图像更简单地表达上面的结构:

微信图片_20191215121202

第 1 步转换的过程中可以验证语法的正确性,同时由字符串变为对象结构后更有利于精准地分析以及进行代码结构调整。

第 2 步原理就很简单了,就是遍历这个对象所描述的抽象语法树,遇到哪里需要做一下改变,就直接在对象上进行操作,即 plugin 用 babel-traverse 对 AST 树进行遍历转译 ==》 得到新的 AST 树。

plugins

插件应用于 babel 的转译过程,尤其是第二个阶段 transforming,如果这个阶段不使用任何插件,那么 babel 会原样输出代码。

我们主要关注 transforming 阶段使用的插件,因为 transform 插件会自动使用对应的词法插件,所以 parsing 阶段的插件不需要配置。

presets

如果要自行配置转译过程中使用的各类插件,那太痛苦了,所以 babel 官方帮我们做了一些预设的插件集,称之为 preset,这样我们只需要使用对应的 preset 就可以了。以 JS 标准为例,babel 提供了如下的一些 preset: es2015 es2016 es2017 env es20xx 的 preset 只转译该年份批准的标准,而 env 则代指最新的标准,包括了 latest 和 es20xx 各年份

另外,还有 stage-0 到 stage-4 的标准成形之前的各个阶段,这些都是实验版的 preset,建议不要使用。

第 3 步也简单,递归遍历这颗语法树,然后生成相应的代码 代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps),代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。

我们可以使用 npm 中的第三方模块来模拟 BaBel 这三个过程这个过程。尝试修改函数的名字。

​
// 解析  
let esprima = require('esprima')  
// 转换  
let estraverse = require('estraverse')  
// 生成  
let escodegen = require('escodegen')

let code = `function code () {}`  
// 解析  
let ast = esprima.parseScript(code)  
// 转换  
estraverse.traverse(ast, {  
 enter (node) {  
 console.log('enter' + node.type)  
 if (node.type === 'enterIdentifier') {  
 node.name = 'hello'  
 }  
 },  
 leave (node) {  
 console.log('enter' + node.type)  
 }  
})  
// 生成  

let newCode = escodegen.generate(ast)

其实 Babel 工作过程就是解析,转换,生成三个过程将 ES6+语法转换成浏览器能识别的 ES5 语法,这个也是面试常谈的,当然咱们面试中不可能会这么长篇大论,用简便的回答就是以下这个思路:

ES6代码输入 ==》 babylon进行解析 ==》 得到AST
==》 plugin用babel-traverse对AST树进行遍历转译 ==》 得到新的AST树
==》 用babel-generator通过AST树生成ES5代码`

感兴趣的童鞋可以关注微信公众号【入坑互联网】获取更多前端干货及免费海量学习资料