babel原理浅析

719 阅读6分钟

目前的前端项目里,如果不是那些上古世纪留下的代码,最新的项目大家应该都选择vue或者react开发,然后运用各种炫酷的ES6语法,开发起来那叫一个爽。

在使用ES6语法的时候,我们需要引用的一个插件就是babel, 大家应该对babel不陌生,但是,你真的知道它背后的原理吗? 今天我们就来探索一下,让我们使用E6飞起的背后的男人:babel

一些概念

在了解babel背后的原理之前,我们先来聊聊babel背后的一些概念

什么是babel

Babel 是一个 JavaScript 编译器,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript

比如下面的代码:

[1,2,3].map(n => n + 1);

经过babel编译之后

[1, 2, 3].map(function (n) {
  return n + 1;
});

上面的代码中,babel经历了以下三个过程:

解析(parse)--> 转换(transform)--> 生成(generate)

中间的那个,叫抽象语法树(AST)

AST

代码解析成AST的目的是为了方便计算机更好地理解我们的代码,那么什么是AST?

官方定义:

在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。

看完一脸懵逼,我们还是来看个例子吧

AST在线转换

const a = 1 + 3

经过转换后为

{
  "type": "Program",
  "start": 0,
  "end": 15,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 15,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 15,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 7,
            "name": "a"
          },
          "init": {
            "type": "BinaryExpression",
            "start": 10,
            "end": 15,
            "left": {
              "type": "Literal",
              "start": 10,
              "end": 11,
              "value": 1,
              "raw": "1"
            },
            "operator": "+",
            "right": {
              "type": "Literal",
              "start": 14,
              "end": 15,
              "value": 3,
              "raw": "3"
            }
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}

通过例子,我们就比较好理解什么是AST了,它是上面代码中const a = 1 + 3的树状表现形式

type: xxx,
start: xx
end: xx

上面代码的结构,叫做节点(Node)。一个AST是由多个或单个这样的节点组成,节点内部可以有多个这样的子节点,构成一颗语法树,这样就可以描述用于静态分析的程序语法。 节点中的type字段表示节点的类型,比如上述AST中的"VariableDeclarator"、"Identifier"、"Literal"等等,当然每种节点类型会有一些附加的属性用于进一步描述该节点类型。

babel的运行原理

理解了上面的一些概念,接下来我们来了解一下babel的运行原理

第一步:解析(parse)

很多js引擎都有自己的AST解析器,比如谷歌的v8引擎。而Babel是通过Babylon实现的。

在解析过程中有两个阶段:词法分析语法分析

词法分析阶段:

要做词法分析,我们首先需要知道,在js中,哪些属于语法单元:

  1. 数字:JavaScript 中的科学记数法以及普通数组都属于语法单元.
  2. 括号:『(』『)』只要出现,不管任何意义都算是语法单元
  3. 标识符:连续字符,常见的有变量,常量(例如: null true),关键字(if break)等
  4. 运算符:+、-、*、/等等
  5. 一些注释,中括号等

我们在解析代码的过程中,可以将代码理解为一段字符串,词法分析阶段把字符串形式的代码转换成令牌(tokens)流

令牌(tokens)流又是什么鬼?

你可以把它看作是一个扁平化的语法片段数组,类似于AST中的节点

如:1+3 代码经过词法分析转换成令牌流

// 1+3
[
  { type: { ... }, value: "1", start: 0, end: 1, loc: { ... } },
  { type: { ... }, value: "+", start: 2, end: 3, loc: { ... } },
  { type: { ... }, value: "3", start: 4, end: 5, loc: { ... } },
  ...
]

每个type都有一组属性来描述该令牌:

{
  type: {
    label: 'name',
    keyword: undefined,
    beforeExpr: false,
    startsExpr: true,
    rightAssociative: false,
    isLoop: false,
    isAssign: false,
    prefix: false,
    postfix: false,
    binop: null,
    updateContext: null
  },
  ...
}

语法分析

语法分析阶段则会把一个令牌流转换成AST的形式,同时这个阶段会把令牌中的信息转换成AST的表述结构。

语法分析比词法分析要复杂,因为要分析各种语法的可能性,需要开发者根据令牌流提供的信息来分析出代码之间的逻辑关系,只有经过词法分析令牌流才能成为有结构的抽象语法树. 语法分析一般会依照一个标准,大多数 JavaScript Parser 都遵循estree规范

第二阶段:转换(transform)

通过第一步的解析,babel将js代码解析成了AST,在第二个阶段,babel接受AST并通过babel-traverse对其进行深度优先遍历,在此过程中对节点进行添加、更新及移除操作。这部分也是Babel插件介入工作的部分,此时,得到的是新的AST树。

听起来起来有点抽象,举个例子

比如我们要用 babel 做一个Vue转小程序的转换器, babel工作流程的粗略情况是这样的:

babel 将 vue 代码解析为AST抽象语法树,开发者利用 babel 插件定义转换规则,根据原本的抽象语法树生成一个符合小程序规则的新抽象语法树, babel 则根据新的抽象语法树生成代码,此时的代码就是符合小程序规则的新代码

例如 Taro就是用 babel 完成的小程序语法转换.

我们转换代码的关键就是根据当前的抽象语法树,以我们定义的规则生成新的抽象语法树,转换的过程就是生成新抽象语法树的过程.

vue代码输入 ==》 babylon进行解析 ==》 得到AST ==》 plugin用babel-traverse对AST树进行遍历转译 ==》 得到新的AST树 ==》 用babel-generator通过AST树生成符合小程序使用的代码

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

babel官方帮我们做了一些预设的插件集,称之为preset,这样我们只需要使用对应的preset就可以了。以JS标准为例,babel提供了如下的一些preset:

es2015

es2016

es2017

...

第三步:生产(generate)

将经过转换的AST通过babel-generator再转换成js代码,过程就是深度优先遍历整个AST,然后构建可以表示转换后代码的字符串。同时还会创建源码映射(source maps)。

手写一个简单的babel

通过前面的文章,我们知道了babel的解析过程为3步走策略

第一步,由Babylon进行解析为AST

第二步由babel-traverse转义为新的AST

第三步,由babel-generator生成解析后的代码。

下面,我们试着将const a = 1 + 3,通过我们写的babel,生成为var a = 1 + 3

import * as  babylon from "babylon";
import traverse from "babel-traverse";
import generate from "babel-generator";

const code = `const a = 1+3 ;`;
const ast = babylon.parse(code);

traverse(ast, {
  enter(path) {
   if (path.node.type === "VariableDeclaration" && path.node.kind === 'const' ) {
      path.node.kind = 'var';
    }
  }
})

const genCode = generate(ast, {}, code); 

console.log('genCode', genCode); // {code: "var a = 1 + 3;", map: null, rawMappings: null}

一些babel插件的书写也是这个原理,本文粗略探讨了babel的原理以及解析过程,更加详细的内容,可以查看这里:传送门