babel原理及AST

# Babel的运行原理

img

# 解析(parsing)

解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:词法分析(Lexical Analysis) 和 语法分析(Syntactic Analysis)。

# 词法分析

词法分析阶段把字符串形式的代码转换为 令牌(tokens) 流。

你可以把令牌看作是一个扁平的语法片段数组:

n * n;
[
  { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
  { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
  { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
  ...
]
1
2
3
4
5
6
7

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

{
  type: {
    label: 'name',
    keyword: undefined,
    beforeExpr: false,
    startsExpr: true,
    rightAssociative: false,
    isLoop: false,
    isAssign: false,
    prefix: false,
    postfix: false,
    binop: null,
    updateContext: null
  },
  ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

和 AST 节点一样它们也有 start,end,loc 属性。

# 语法分析

语法分析阶段会把一个令牌流转换成 AST 的形式。 这个阶段会使用令牌中的信息把它们转换成一个 AST 的表述结构,这样更易于后续的操作。

简单来说,解析阶段就是

code(字符串形式代码) -> tokens(令牌流) -> AST(抽象语法树)
1

Babel 使用 @babel/parser 解析代码,输入的 js 代码字符串根据 ESTree 规范生成 AST(抽象语法树)。Babel 使用的解析器是 babylon (opens new window)

# 转换(transforming)

转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel 或是其他编译器中最复杂的过程。

Babel提供了@babel/traverse(遍历)方法维护这AST树的整体状态,并且可完成对其的替换,删除或者增加节点,这个方法的参数为原始AST和自定义的转换规则,返回结果为转换后的AST。

# 生成(generation)

代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)

代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。

Babel使用 @babel/generator 将修改后的 AST 转换成代码,生成过程可以对是否压缩以及是否删除注释等进行配置,并且支持 sourceMap。

img

# Babel ES6 转 ES5 为例的过程

# 具体过程
  1. 编写ES6代码
  2. babylon 进行解析
  3. 解析得到 AST
  4. plugin 用 babel-traverse 对 AST 树进行遍历转译
  5. 得到新的 AST树
  6. 用 babel-generator 通过 AST 树生成 ES5 代码

# Babel插件编写

# 基础要点

# babel-core (opens new window)

babel-core是Babel的核心包,里面存放着诸多核心API,这里说下transform。

transform : 用于字符串转码得到AST 。安装 npm install babel-core -D;

import babel from 'babel-core';
/*
 * @param {string} code 要转译的代码字符串
 * @param {object} options 可选,配置项
 * @return {object} 
*/
babel.transform(code:String,options?: Object)
//返回一个对象(主要包括三个部分):
{
    generated code, //生成码
    sources map, //源映射
    AST  //即abstract syntax tree,抽象语法树
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# babel-types (opens new window)

Babel Types模块是一个用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。安装 npm install babel-types -D;

import traverse from "babel-traverse";
import * as t from "babel-types";
traverse(ast, {
  enter(path) {
    if (t.isIdentifier(path.node, { name: "n" })) {
      path.node.name = "x";
    }
  }
});
1
2
3
4
5
6
7
8
9

# JS CODE -> AST

查看代码对应的AST树结构 astexplorer (opens new window)

# Visitors (访问者)

访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。 这么说有些抽象所以让我们来看一个例子。

const MyVisitor = {
  Identifier() {
    console.log("Called!");
  }
};
// 你也可以先创建一个访问者对象,并在稍后给它添加方法。
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}
1
2
3
4
5
6
7
8
9

注意: Identifier() { ... } 是 Identifier: { enter() { ... } } 的简写形式

这是一个简单的访问者,把它用于遍历中时,每当在树中遇见一个 Identifier 的时候会调用 Identifier() 方法。

# Paths(路径)

AST 通常会有许多节点,那么节点直接如何相互关联呢? 我们可以使用一个可操作和访问的巨大可变对象表示节点之间的关联关系,或者也可以用Paths(路径)来简化这件事情。

Path 是表示两个节点之间连接的对象。

在某种意义上,路径是一个节点在树中的位置以及关于该节点各种信息的响应式 Reactive 表示。 当你调用一个修改树的方法后,路径信息也会被更新。 Babel 帮你管理这一切,从而使得节点操作简单,尽可能做到无状态。

Paths in Visitors(存在于访问者中的路径)

当你有一个 Identifier() 成员方法的访问者时,你实际上是在访问路径而非节点。 通过这种方式,你操作的就是节点的响应式表示(译注:即路径)而非节点本身。

const MyVisitor = {
  Identifier(path) {
    console.log("Visiting: " + path.node.name);
  }
};
1
2
3
4
5

# Babel插件规则

Babel的插件模块需要我们暴露一个function,function内返回visitor对象。

//函数参数接受整个Babel对象,这里将它进行解构获取babel-types模块,用来操作AST。
module.exports = function({types:t}){
    return {
        visitor:{}
    }
}
1
2
3
4
5
6

# 插件实践

# 简单的ES6转ES3插件

  1. let,const 声明 -> var 声明
  2. 箭头函数 -> 普通函数
# 通过astexplorer对比参考

实现思路只要进行节点替换(ArrowFunctionExpression->FunctionExpression)就可以

首先来实现 功能 1. let,const 声明 -> var 声明

let code = 1; 通过传送门 (opens new window)查看到上面代码对应的AST结构为

img

可以看到这句声明语句位于VariableDeclaration节点,接下来只要操作VariableDeclaration节点对应的kind属性就可以啦~

接着箭头函数对应的节点是什么

let add = (x, y) => {
  return x + y;
};
1
2
3

通过astexplorer查看到上面代码对应的AST结构为

img

可以看到箭头函数对应的节点是ArrowFunctionExpression

接着看看普通函数对应的节点是什么

let add = function(x, y){
  return x + y;
};
1
2
3

通过astexplorer查看到上面代码对应的AST结构为

img

可以看到普通函数对应的节点是FunctionExpression。

# 核心

index.js

const fs = require("fs");
const { transform } = require("@babel/core");

//读取需要转换的js字符串
const before = fs.readFileSync("./before.js", "utf8");
//使用babel-core的transform API 和插件进行字符串->AST转化。
const res = transform(`${before}`, {
  plugins: [require("./plugin")],
});

// 存在after.js删除
fs.existsSync("./after.js") && fs.unlinkSync("./after.js");
// 写入转化后的结果到after.js
fs.writeFileSync("./after.js", res.code, "utf8");
// console.log('res', res.code);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

plugin.js

module.exports = function ({ types: t }) {
  return {
    //访问者
    visitor: {
      //需要操作的访问者方法(节点)
      VariableDeclaration(path) {
        const node = path.node; //该路径对应的节点
        ["let", "const"].includes(node.kind) && (node.kind = "var"); //判断节点kind属性是let或者const,转化为var
      },
      //箭头函数对应的访问者方法(节点)
      ArrowFunctionExpression(path) {
        //该路径对应的节点信息
        let { id, params, body, generator, async } = path.node;
        //箭头函数我们会简写{return a+b} 为 a+b
        if (!t.isBlockStatement(body)) {
          const node = t.returnStatement(body);
          body = t.blockStatement([node]);
        }
        //进行节点替换 (arrowFunctionExpression->functionExpression)
        path.replaceWith(
          t.functionExpression(id, params, body, generator, async)
        );
      },
    },
  };
};
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

before/after.js

// before.js
let div = (x, y) => x / y;
const add = (a, b) => {
  return a + b;
};

//after.js
var div = function (x, y) {
  return x / y;
};
var add = function (a, b) {
  return a + b;
};
1
2
3
4
5
6
7
8
9
10
11
12
13

# AST

# 简介

抽象语法树(AST), 抽象语法树(abstract syntax tree, AST) 的数据结构。

AST 不仅以结构化的方式显示源代码,而且在语义分析中扮演着重要角色。在语义分析中,编译器验证程序和语言元素的语法使用是否正确。之后,使用 AST 来生成实际的字节码或者机器码。

抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。和抽象语法树相对的是具体语法树(concrete syntaxtree),通常称作分析树(parse tree)。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树。一旦 AST 被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。

# JavaScript 解析

让我们看看 AST 是如何构建的。我们用一个简单的 JavaScript 函数作为例子:

function foo(x) {
    if (x > 10) {
        var a = 2;
        return a * x;
    }
    return x + 10;
}
1
2
3
4
5
6
7

解析器会产生如下的 AST:

图片描述

注意,为了观看方便,这里是解析器将生成的结果的简化版本。实际的 AST 要复杂得多。然而,这里的目的是为了运行源码之前的第一个步骤前。如果人想查看实际的 AST 是什么样子,可以访问 AST Explorer (opens new window)。它是一个在线工具,你以在其中输入一些 JavaScript 并输出对应的 AST。

# 类的转换

当 ES6 或 ECMAScript 2015 中的新特性被提出时,JavaScript 开发人员不能等待所有引擎和浏览器都开始支持它们。为实现浏览器能够支持新的特性一个好方法是通过 转换 (Transpiling) ,它允许将 ECMAScript 2015 中编写的代码转换成任何浏览器都能理解的 JavaScript 代码,当然也包括使用基于类的继承编写类的转换功能。

图片描述

# 使用Babel转换

最流行的 JavaScript 编译器之一就是 Babel,宏观来说,它分3个阶段运行代码:解析(parsing),转译(transforming),生成(generation),来看看它是如何转换的:

class Component {
  constructor(content) {
    this.content = content;
  }

  render() {
      console.log(this.content)
  }
}

const component = new Component('SessionStack');
component.render();
1
2
3
4
5
6
7
8
9
10
11
12

以下是 Babel 转换后的样式:

var Component = function () {
  function Component(content) {
    _classCallCheck(this, Component);
    this.content = content;
  }

  _createClass(Component, [{
    key: 'render',
    value: function render() {
      console.log(this.content);
    }
  }]);

  return Component;
}();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

如上所见,转换后的代码就可在任何浏览器执行了。 此外,还添加了一些功能, 这些是 Babel 标准库的一部分。

_classCallCheck_createClass 作为函数包含在编译文件中。

  • _classCallCheck 函数的作用在于确保构造方法永远不会作为函数被调用,它会评估函数的上下文是否为 Component 对象的实例,以此确定是否需要抛出异常。
  • _createClass 用于处理创建对象属性,函数支持传入构造函数与需定义的键值对属性数组。函数判断传入的参数(普通方法/静态方法)是否为空对应到不同的处理流程上。

为了探究继承的实现原理,分析继承的 ComponentInputField 类。。

class InputField extends Component {
    constructor(value) {
        const content = `<input type="text" value="${value}" />`;
        super(content);
    }
}
1
2
3
4
5
6

使用 Babel 处理上述代码,得到如下代码:

 var InputField = function (_Component) {
 _inherits(InputField, _Component);

 function InputField(value) {
    _classCallCheck(this, InputField);

    var content = '<input type="text" value="' + value + '" />';
    return _possibleConstructorReturn(this, (InputField.__proto__ || Object.getPrototypeOf(InputField)).call(this, content));
  }

  return InputField;
}(Component);
1
2
3
4
5
6
7
8
9
10
11
12

在本例中, Babel 创建了 _inherits 函数帮助实现继承。

以 ES6 转 ES5 为例,具体过程:

  1. 编写ES6代码
  2. babylon 进行解析
  3. 解析得到 AST
  4. plugin 用 babel-traverse 对 AST 树进行遍历转译
  5. 得到新的 AST树
  6. 用 babel-generator 通过 AST 树生成 ES5 代码

# 抽象语法树

AST 包含多个节点,且每个节点只有一个父节点。 在 Babel 中,每个形状树的节点包含可视化类型、位置、在树中的连接等信息。 有不同类型的节点,如 stringnumbersnull等,还有用于流控制(if)和循环(for,while)的语句节点。 并且还有一种特殊类型的节点用于类。它是基节点类的一个子节点,通过添加字段来扩展它,以存储对基类的引用和作为单独节点的类的主体。

把下面的代码片段转换成一个抽象语法树:

class Component {
  constructor(content) {
    this.content = content;
  }

  render() {
    console.log(this.content)
  }
}
1
2
3
4
5
6
7
8
9

下面是以下代码片段的抽象语法树:

图片描述

Babel 的三个主要处理步骤分别是: 解析(parse),转换 (transform),生成 (generate)。

# 解析

将代码解析成抽象语法树(AST),每个js引擎(比如Chrome浏览器中的V8引擎)都有自己的AST解析器,而Babel是通过 Babylon 实现的。在解析过程中有两个阶段: 词法分析 和 语法分析 ,词法分析阶段把字符串形式的代码转换为 令牌 (tokens)流,令牌类似于AST中节点;而语法分析阶段则会把一个令牌流转换成 AST的形式,同时这个阶段会把令牌中的信息转换成AST的表述结构。

# 转换

在这个阶段,Babel接受得到AST并通过babel-traverse对其进行 深度优先遍历 (opens new window),在此过程中对节点进行添加、更新及移除操作。这部分也是Babel插件介入工作的部分。

# 生成

将经过转换的AST通过babel-generator再转换成js代码,过程就是 深度优先遍历 (opens new window)整个AST,然后构建可以表示转换后代码的字符串。

在上面的示例中,首先生成两个 MethodDefinition 节点的代码,然后生成类主体节点的代码,最后生成类声明节点的代码。

# 使用 TypeScript 进行转换

另一个利用转换的流行框架是 TypeScript。它引入了一种用于编写 JavaScript 应用程序的新语法,该语法被转换为任何浏览器或引擎都可以执行的 EMCAScript 5。下面是用 Typescript 实现 Component :

class Component {
    content: string;
    constructor(content: string) {
        this.content = content;
    }
    render() {
        console.log(this.content)
    }
}
1
2
3
4
5
6
7
8
9

转成抽象语法树如下:

图片描述

Typescript 还支持继承:

class InputField extends Component {
    constructor(value: string) {
        const content = `<input type="text" value="${value}" />`;
        super(content);
    }
}
1
2
3
4
5
6

以下是转换结果:

var InputField = /** @class */ (function (_super) {
    __extends(InputField, _super);
    function InputField(value) {
        var _this = this;
        var content = "<input type=\"text\" value=\"" + value + "\" />";
        _this = _super.call(this, content) || this;
        return _this;
    }
    return InputField;
}(Component));
1
2
3
4
5
6
7
8
9
10

最终的结果还是 ECMAScript 5 代码,其中包含 TypeScript 库中的一些函数。封 __extends 中的逻辑与在第一节中讨论的逻辑相同。

随着 Babel 和 TypeScript 被广泛采用,标准类和基于类的继承成为了构造 JavaScript 应用程序的标准方式,这推动了在浏览器中引入对类的原生支持。

# 类的原生支持

2014年,Chrome 引入了对 类的原生支持 (opens new window),这允许在不需要任何库或转换器的情况下执行类声明语法。

图片描述

本地实现类的过程就是我们所说的语法糖。这只是一种奇特的语法,它可以编译成语言中已经支持的相同的原语。可以使用新的易于使用的类定义,但是它仍然会创建构造函数和分配原型。

图片描述

# 相关链接

https://segmentfault.com/a/1190000016231512

上次更新: 2022/04/15, 05:41:30
×