Babel 内部机制研究

Babel

Babel 是一个 JavaScript 编译器。node

// Babel 输入: ES2015 箭头函数
[1, 2, 3].map((n) => n + 1);

// Babel 输出: ES5 语法实现的同等功能
[1, 2, 3].map(function(n) {
  return n + 1;
});
复制代码

Babel经过转换,让咱们写新版本的语法,转换到低版本,这样就能够在只支持低版本语法的浏览器里运行了。git

Babel真厉害,它竟然‘认识’代码、更改代码。那Babel就是操做代码的代码,酷。github

学习Babel对咱们能力的提高有很大帮助,咱们平时都是用代码来操做各类东西,此次咱们操做的对象,变成了代码自己。算法

这个认识和操做代码的过程,学名叫作代码的静态分析。express

代码的静态分析

先来看一个问题,编辑器里代码的高亮,以下:npm

image

编辑器能够把代码不一样的成员,标记为不一样的颜色。显然编辑器要‘认识’代码,对代码进行分析和处理,才能达到这种效果。这就叫代码的静态分析。浏览器

静态分析 VS 动态分析

静态分析是在不须要执行代码的前提下对代码进行分析的处理过程。bash

动态分析是在代码的运行过程当中对代码进行分析和处理。上面的代码高亮属于静态分析,在代码没有运行的状况下,进行分析和处理的。babel

静态分析的用处

静态分析不光能高亮代码,还有就是代码转换,还能够对咱们的源代码进行优化、压缩等操做。数据结构

AST (抽象语法树)

在对代码静态分析的过程当中,要将源码转换成AST (抽象语法树)。

为何会有AST

源代码对于Babel来讲,就是一个字符串。Babel要对这个字符串进行分析。咱们平时对字符串的操做,就是使用字符串方法或是正则,但相对字符串(源码)进行复杂的操做,远远不够。

须要将字符串(源码)转换成树的数据结构,才好操做。这个树结构,就叫AST(抽象语法树)。

这里我想到 程序 = 数据结构 + 算法。咱们平时写的业务需求,对数据结构要求不高,简单的对象和列表就能够搞定,但要某些特定的复杂问题,好比如今研究的操做代码,就需先思考:我应该把操做的事物放到什么样的数据结构上,才更容易我写算法/逻辑。

把源码解析成AST

对于源码,此时咱们就把它看出一个字符串,对其分析的第一步,确定是先把源码转换成AST,才好后续操做。

有一个在线AST转换器,咱们在这上面能够作实验,写出的代码,它就帮咱们翻译成AST:

我什么都不写,AST就有一个根结点了:

// AST
{
  "type": "Program",
  "start": 0,
  "end": 0,
  "body": [],
  "sourceType": "module"
} // 能够当作是一个对象,有一些字段,这代码树的根结点。
复制代码

而后我写一句代码:

// 源码
const text = 'Hello World';

// AST
{
  "type": "Program",
  "start": 0,
  "end": 27,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 27,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 26,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 10,
            "name": "text"
          },
          "init": {
            "type": "Literal",
            "start": 13,
            "end": 26,
            "value": "Hello World",
            "raw": "'Hello World'"
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}
复制代码

懵逼,一句const text = 'Hello World'; 就生成这么多东西。看懂它、理解AST是学习Babel的第一个门槛。

理解AST

看下图,这就是AST Explorer的界面,左边写代码,右边就帮助咱们翻译成AST,AST有两种表达方式,Tree和JSON,上面都是用JSON形式表示AST,后来我发现仍是用Tree的形式看更容易些,由于Tree的形式更突出节点的类型:

image

我将AST表达的树画出来,以下:

image

总结AST树的特色:

  1. 节点是有类型的。咱们学习树这种数据结构时,节点都是最简单的,这里复杂了,有类型。
  2. 节点与子节点的关系,是经过节点的属性连接的。咱们学习的树结构,都是left、right左孩子右孩子的。可是AST树,不一样类型的节点,属性不一样,Program类型节点的子节点是它的body属性,VariableDeclaration类型的子节点,是它的declarations、kind属性。也就是节点的属性看做是节点的子节点,而且子节点也可能有类型,近而造成一个树。
  3. 父节点是全部子节点的组合,咱们能够看到VariableDeclaration表明的const text = 'Hello World'被拆分红了下面两个子节点,子节点又继续拆分。

但愿能从上面的分析中,让你们对AST有一个最直观的认识,就是节点有类型的树。

那么节点的类型系统就很必要了解了,这里是Babel的AST类型系统说明。你们能够看看,能够说类型系统是抽象了代码的各类成员,标识符、字面量、声明、表达式。因此拥有这些类型的节点的树结构,能够用来表达咱们的代码。

参照类型系统,多实验,咱们就会对AST的结构大致掌握和理解了。

额外:V8引擎也用到AST

额外提一下,V8中也用到了AST。V8引擎有四个主要模块:

  1. 转换器Paser:将源代码转换成AST。
  2. 解释器:将AST转换为Bytecode。
  3. 编译器:将Bytecode转换为汇编代码。
  4. 垃圾回收模块:负责管理内存空间回收。

能够看到AST也是V8执行的关键一环。下面下来看看Babel对于AST的利用,及运行步骤。

Babel 的处理步骤

回看Babel的处理过程

  1. 解析(parse)。将源代码变成AST。
  2. 转换(transform)。操做AST,这也是咱们能够操做的部分,去改变代码。
  3. 生成(generate)。将更改后的AST,再变回代码。

解析器 babylon

第一步:解析,Babel中的解析器是babylon。咱们来体验一下:

// 安装
npm install --save babylon
复制代码
// 实验代码
import * as babylon from "babylon";

const code = `const text = 'Hello World';`;

const ast = babylon.parse(code);

console.log('ast', ast);

复制代码

code变量是咱们的源代码,ast变量是AST,咱们看一下打印结果:

image

和咱们预期的同样,获得AST了。这里我注意到还有start、end、loc这样位置信息的字段,应该能够对生成Source Map有用的字段。

转换器 babel-traverse

第二步:转换。获得ast了,该操做它了,Babel中的babel-traverse用来干这个事。

// 安装
npm install --save babel-traverse
复制代码
// 实验代码
import * as babylon from "babylon";
import traverse from "babel-traverse";

const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);

traverse(ast, {
  enter(path) {
    console.log('path', path);
  }
})

console.log('ast', ast);
复制代码

babel-traverse库暴露了traverse方法,第一个参数是ast,第二个参数是一个对象,咱们写了一个enter方法,方法的参数是个path,咋不是个node呢?咱们看一下输出:

image

path被打印了5次,ast上确实也是有5个节点,是对应的。traverse方法是一个遍历方法,path封装了每个节点,而且还提供容器container,做用域scope这样的字段。提供个更多关于节点的相关的信息,让咱们更好的操做节点。

咱们来作一个变量重命名操做:

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

const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);

traverse(ast, {
  enter(path) {
    if (path.node.type === "Identifier"
      && path.node.name === 'text') {
      path.node.name = 'alteredText';
    }
  }
})

console.log('ast', ast);
复制代码

看结果:

image

确实咱们的ast被更改了,用这个ast生成的code就会是const alteredText = 'Hello World';

babel-traverse的Lodash : babel-types

在利用babel-traverse操做AST时,也能够利用工具库帮助咱们写出更加简洁有效的代码,就可使用babel-types。

npm install --save babel-types
复制代码
import * as babylon from "babylon";
import traverse from "babel-traverse";
import * as t from "babel-types";

const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);

traverse(ast, {
  enter(path) {
    // 使用babel-types
    if (t.isIdentifier(path.node, { name: "text" })) {
      path.node.name = 'alteredText';
    }
  }
})

console.log('ast', ast);
复制代码

使用babel-types实现和上例中同样的功能,代码量更少了。

生成器 babel-generator

第三步:生成。获得操做后的ast,该生成新代码了。Babel中的babel-generator用来干这个事。

npm install --save babel-generator
复制代码
// 加入babel-generator
import * as babylon from "babylon";
import traverse from "babel-traverse";
import * as t from "babel-types";
import generate from "babel-generator";

const code = `const text = 'Hello World';`;
const ast = babylon.parse(code);

traverse(ast, {
  enter(path) {
    if (t.isIdentifier(path.node, { name: "text" })) {
      path.node.name = 'alteredText';
    }
  }
})

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

console.log('genCode', genCode);
复制代码

来看打印结果:

image

nice! 在code字段里,咱们看到里新生成的代码。

固然,上面提到的这四个库,还有更多细节,有兴趣的能够再研究研究。

这些库的集合,就是咱们的Babel。

插件

咱们研究过了Babel的内部,如今跳出来,咱们须要在外部操做AST,而不是进Babel内部去改traverse。

从外部操做AST,就须要插件了。咱们来研究一个插件babel-plugin-transform-member-expression-literals

使用次插件后:

// 转换前
obj.const = "isKeyword";

// 转换后
obj["const"] = "isKeyword";
复制代码

咱们再来看看这个插件的源码

{
    name: "transform-member-expression-literals",
    visitor: {
      MemberExpression: {
        exit({ node }) {
          const prop = node.property;
          if (
            !node.computed &&
            t.isIdentifier(prop) &&
            !t.isValidES3Identifier(prop.name)
          ) {
            // foo.default -> foo["default"]
            node.property = t.stringLiteral(prop.name);
            node.computed = true;
          }
        },
      },
    },
  };
}
复制代码

这里我尝试将它放到咱们的实验代码里,以下:

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

const code = `const obj = {};obj.const = "isKeyword";`;
const ast = babylon.parse(code);
const plugin = {
  MemberExpression: {
    exit({ node }) {
      const prop = node.property;
      console.log('node', node);
      if (
        !node.computed &&
        t.isIdentifier(prop)
        // !t.isValidES3Identifier(prop.name) 这里注释掉,咱们的t里没这个方法
      ) {
        // foo.default -> foo["default"]
        node.property = t.stringLiteral(prop.name);
        node.computed = true;
      }
    },
  },
};

traverse(ast, plugin)

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

console.log('genCode', genCode); 
复制代码

输出的代码是"const obj = {};obj["const"] = "isKeyword";"。符合预期,也就是说,Babel的插件,会传给内部的traverse方法。而且是一种符合访问者模式的,让咱们能够针对节点类型(如这里的visitor.MemberExpression)的操做。这里用的是exit,而不是enter了,解释一下,traverse是对树的深度遍历,向下遍历这棵树咱们进入(entry)每一个节点,向上遍历回去时咱们退出(exit)每一个节点。就是对于AST,traverse遍历了两遍,咱们能够选择在进入仍是退出的时候,操纵节点。

结束语

个人参考:

  1. Babel用户手册
  2. Babel中文文档

今天的研究就到这里,理解Babel内部机制和基本的插件工做方式。

相关文章
相关标签/搜索