目录

Babel简介

1.babel 简介

Babel是现代JavaScript语法转换器,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中,包括不限于: eslint jsx vue-template等等。他能为你做的:语法转换、通过 Polyfill 方式在目标环境中添加缺失的特性、源码转换。可以说,通过babel,我们可以使用最新的语法或者特性专注的开发业务,而不用将精力花在代码的兼容上

2.babel的解析流程

2.1.词法解析

首先由入口文件@babel/core引入各种配置文件以及解析的模块,其中负责解析基本语法的为parse模块,其中就定义了词法解析器Tokenizer,可以看出初始化时会有一个tokens数组,在这个阶段会把字符串形式的代码转换为一个数组,tokens可以视为由拆分为各个片段组成的数组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Tokenizer extends LocationParser {
  constructor(options, input) {
    super();
    this.tokens = [];
    this.state = new State();
    this.state.init(options);
    this.input = input;
    this.length = input.length;
    this.isLookahead = false;
  }
	
  ....
}

Tokenizer定义中还有一些其它的方法,他们会对各种语法字符串进行一个遍历过滤,比如跳过空格字符串,比如跳过注释等等

1
2
3
4
5
6
7
8
skipSpace() {
  loop: while (this.state.pos < this.length) {
    const ch = this.input.charCodeAt(this.state.pos);
    .
    .
    .
  }
}

最后会把符合规则的语法字符串加入到tokens数组中,也就生成代码拆分为各个片段组成的数组

1
2
3
4
5
pushToken(token) {
  this.tokens.length = this.state.tokensLength;
  this.tokens.push(token);
  ++this.state.tokensLength;
}

如下所示:

1
console.log('我是一段语法') => ['console', 'log', '(', '"我是一段语法"', ')']

2.2.语法解析

当生成完tokens,则进入到语法解析阶段,同样可以看见在parse模块中定一个了一个Node对象,这个对象定义了一些属性,当然还有个私有的_clone方法,用于表示将要生成的AST中节点的位置、名称、类型等等信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Node {
  constructor(parser, pos, loc) {
    this.type = "";
    this.start = pos;
    this.end = 0;
    this.loc = new SourceLocation(loc);
    if (parser && parser.options.ranges) this.range = [pos, 0];
    if (parser && parser.filename) this.loc.filename = parser.filename;
  }

  __clone() {
    .
    .
    .
  }

}

然后会定义相应的Parse,遍历处理不同的tokens字段,比如ImportDeclaration用于import语法,导入模块;VariableDeclarator用于处理表示是什么类型的变量声明,比如var、let、constFunctionDeclaration用于处理函数声明,非函数表达式,其它的就不多赘述。最后则会将这个词法数组转换为AST(抽象语法树)

1
2
3
var add = function(a, b) {
  return  a + b
}

最后生成的AST简易版本如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
  "type": "Program",
  "body": [{
    "type": "VariableDeclaration",
    "identifierName": "add",
    "init": {
      "type": "ArrowFunctionExpression",
      "params": [{
          "type": "identifier",
          "identifierName": "a"
        },
        {
          "type": "identifier",
          "identifierName": "b"
        }
      ],
      "body": {
        "type": "BinaryExpression",
        "left": {
          "type": "identifier",
          "identifierName": "a"
        },
        "operator": "+",
        "right": {
          "type": "identifier",
          "identifierName": "b"
        }
      }
    }
  }]
}

可以看出这个语法树包含了整个语法的一个层级关系,并且标注了他们的名称、类型以及位置

2.3.Traverser

Traverser会对生成好AST进行一个遍历,在此过程中对节点进行添加、删除等等操作,这也是其他插件将要处理的地方,通过这些插件,也可以处理不同的AST语法树,然后转换符合相应规则的代码,比如Taro,就可以视为将React代码转换成小程序对应代码的一个插件。Traverser中会引入visitors,用它来进行一个深度遍历操作;Path用于关联各个节点,这样使得节点操作简单,例如:

例如,如果有下面这样一个节点及其子节点︰

1
2
3
4
5
6
7
8
{
  type: "ArrowFunctionExpression",
  id: {
    type: "Identifier",
    name: "a"
  },
  ...
}

将子节点 Identifier 表示为一个路径(Path)的话,看起来是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "parent": {
    "type": "ArrowFunctionExpression",
    "id": {...},
    ....
  },
  "node": {
    "type": "Identifier",
    "name": "a"
  }
}

Scope用来表示树中各个节点的一个作用域关系,就好像js语法中,创建变量、函数时所呈现的一个作用域效果,在babel中,新添加的引用或者变量名,Scope表示的时候需要体现出节点的路径,节点间的关系,如以下形式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  path: path,
  block: path.node,
  parentBlock: path.parent,
  parent: parentScope,
  bindings: [...]
  .
  .
  .
}

还有个很重要的概念BindingsBindings可以获取当前作用域下所有的标识符,其返回的信息包括节点的标识符scopepath引用等等信息,通过对这些属性的操作,达到对节点信息的一个更改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export class Binding {
  identifier: t.Identifier;
  scope: Scope;
  path: NodePath;
  kind: "var" | "let" | "const" | "module";
  referenced: boolean;
  references: number;              // 被引用的数量
  referencePaths: NodePath[];      // 获取所有应用该标识符的节点路径
  constant: boolean;               // 是否是常量
  constantViolations: NodePath[];
}

2.4.Transform

transform会对抽象语法树进行又一次遍历,针对已经处理好的AST做进一步处理,如对代码的更改,对节点的操作、节点的增删改查、压缩代码、删除注释等等。得到最终的一个AST

2.5.生成代码

得到上一步生成的AST之后,会调用对应的代码生成器代码,通过递归遍历生成最终的代码,其中遇到不同的节点类型时,会做不同的处理,比如函数类型、参数定义类型、代码块类型等等

3.babel里面的一些依赖包

3.1.@babel/core

@babel/core整个babel的一个核心,也算是babel的一个入口,它会加载和处理用户定义的配置,加载各种各样的插件;调用Parser进行语法解析,生成Tokens以及AST;调用Traverser遍历AST,进行一个转换;最后generator生成源代码

3.2.插件

语法插件:babel有很多语法插件,用于支持JavaScript的各种语法特性,在解析的时候会用的到,通常其形式为@babel/plugin-syntax-*,比如babel-plugin-syntax-dynamic-import就是用来处理import语法的一个插件

转换插件: 用于对 AST 进行转换, 实现转换为ES5代码、压缩、功能增强等目的,babel仓库将转换插件划分为两种(只是命名上的区别):

@babel/plugin-transform-*babel中的一个转换插件

预定义的插件:插件集合或者分组,可设置项目内所用插件的使用场景,主要方便用户对插件进行管理和使用。

@babel/preset-envpreset-env是ES语法插件的合集,在根目录下创建.babelrc配置文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 {
   "presets": [
     ["@babel/preset-env", {
       "modules": false,
       "targets": {
         "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
       },
       "useBuiltIns": false,
       "corejs": false
     }]
   ]
 }

targets生成指定环境的代码,useBuiltIns配合@babel/polyfill使用,当然最新版的babel可直接使用corejs,填上对应的数值就行,modules表示是否将代码ES6的模块语法转换为另一种类型或标准,比如amdcommonjs

@babel/polyfill顾名思义,在babel转化的过程中,对于一些无法处理的特性或者属性,使用@babel/polyfill来对这些功能进行处理;

@babel/runtime一般应用于两种场景:开发类库/工具(生成不污染全局空间和内置对象原型的代码)、借助 @babel/runtime 中帮助函数(helper function)移除冗余工具函数,在最新的版本中,完全可以用@babel/runtime代替@babel/polyfill

3.3.辅助开发工具

@babel/template: 某些场景直接操作AST太麻烦,就比如我们直接操作DOM一样,所以babel实现了这么一个简单的模板引擎,可以将字符串代码转换为AST。比如在生成一些辅助代码(helper)时会用到这个库

@babel/types: 主要用途是在创建AST的过程中判断各种语法的类型。

@babel/helper-*: 一些辅助器,用于辅助插件开发,例如简化AST操作

@babel/helper: 辅助代码,单纯的语法转换可能无法让代码运行起来,比如低版本浏览器无法识别class关键字,这时候需要添加辅助代码,对class进行模拟。

4.参考链接

1.深入浅出Babel

2.babel中文网

3.babel-handbook