AST(Abstract Syntax Tree)抽象语法树,或简称语法树(Syntax tree). 是源代码语法结构的一种抽象表示. 它以树状的形式表现编程语言的语法结构, 树上的每一个节点都表示源代码中的一种结构.

1
2
3
4
5
6
while b ≠ 0
if a > b
a := a − b
else
b := b − a
return a

之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于 if-condition-then 这样的条件跳转语句,可以使用带有三个分支的节点来表示。

和抽象语法树相对的是具体语法树(通常称作分析树)。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树,然后从分析树生成AST。一旦AST被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。

AST 能做什么

  • 语法检查、代码风格检查、格式化代码、语法高亮、错误提示、自动补全等.
  • 代码混淆压缩.
  • 优化变更代码,改变代码结构等.

AST-JavaScript

javascript parser 把JS代码转换成 抽象语法树的解析器.浏览器在执行JS之前会把源码通过解析器转化成抽象语法树,再进一步转换成字节码甚至机器码.

常用的javascript parser有:

在整个解析过程分为两部分:

  • 分词: 将整个代码字符串分割成最小语法单元数组
  • 语法分析: 在分词基础上建立分析语法单元之间的关系

语法单元

语法单元:被解析语法中具备实际意义的最小单元.

例如:“2019年是祖国70周年”. 这句话拆成最小单元是: “2019年”,“是”,“祖国”,“70”,“周年”. 这些语法单元再进行拆分就失去了它本来要表达的意思.

javascript代码中的语法单元主要包含一下几种:

  • 关键字: let,const,var
  • 标识符: 没有被括号引起的连续字符串,可能是变量,也可能是if,else这些关键字
  • 数字运算符: +-*%
  • 数字: 十六进制、十进制、八进制等以及科学表达式等语法
  • 空格: 连续的空格、换行、制表符等
  • 注释: 行注释和大块的块注释作为最小的语法单元
  • 其他: 大括号,小括号,分号,冒号等.

例子🌰:

1
(1+2)*3;

通过分词,我们得到一下内容:

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
[
{
"type": "Punctuator",
"value": "("
},
{
"type": "Numeric",
"value": "1"
},
{
"type": "Punctuator",
"value": "+"
},
{
"type": "Numeric",
"value": "2"
},
{
"type": "Punctuator",
"value": ")"
},
{
"type": "Punctuator",
"value": "*"
},
{
"type": "Numeric",
"value": "3"
}
]

在线分词工具:esprima/parser

语法分析

分词后得到一个个独立的语法单元, 这些语法单元需要建立其实际的关系才有意义. 建立语法单元的这个过程就是语法分析,这个一般是递归过程.

(1+2)*3; 最后的语法分析结果是:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{
"type": "Program",
"start": 0,
"end": 186,
"body": [
{
"type": "ExpressionStatement",
"start": 178,
"end": 186,
"expression": {
"type": "BinaryExpression",
"start": 178,
"end": 185,
"left": {
"type": "BinaryExpression",
"start": 179,
"end": 182,
"left": {
"type": "Literal",
"start": 179,
"end": 180,
"value": 1,
"raw": "1"
},
"operator": "+",
"right": {
"type": "Literal",
"start": 181,
"end": 182,
"value": 2,
"raw": "2"
}
},
"operator": "*",
"right": {
"type": "Literal",
"start": 184,
"end": 185,
"value": 3,
"raw": "3"
}
}
}
],
"sourceType": "module"
}

astexplorer 在线可视化 AST工具.

body表示程序体,而程序体中包含了一则表达式ExpressionStatement, 表达式体里包含了操作符 *,以及左右两边表达式,其中右边是数字3,而左边表达式还包含一层表达式,里面是一个+ 操作符,以及左右两边分别为12的数字。

recast - 操作AST瑞士军刀

recast可以将 JS代码块解析成 AST,并针对AST进行JS代码的修改.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
const recast = require('recast')

const code = `
function add(a,b) {
return a+b;
}
`
const ast = recast.parse(code);

const add = ast.program.body[0]

console.log(add)
/**
FunctionDeclaration {
type: 'FunctionDeclaration',
id: Identifier {
type: 'Identifier',
name: 'add',
loc: {
start: [Object],
end: [Object],
lines: [Lines],
tokens: [Array],
indent: 2
}
},
params: [
Identifier { type: 'Identifier', name: 'a', loc: [Object] },
Identifier { type: 'Identifier', name: 'b', loc: [Object] }
],
body: BlockStatement {
type: 'BlockStatement',
body: [ [ReturnStatement] ],
loc: {
start: [Object],
end: [Object],
lines: [Lines],
tokens: [Array],
indent: 2
}
},
generator: false,
expression: false,
async: false,
loc: {
start: { line: 2, column: 2, token: 0 },
end: { line: 4, column: 3, token: 14 },
lines: Lines {
infos: [Array],
mappings: [],
cachedSourceMap: null,
cachedTabWidth: undefined,
length: 5,
name: null
},
tokens: [
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object]
],
indent: 2
}
}
*/


console.log(add.params[0])
/*
Identifier {
type: 'Identifier',
name: 'a',
loc: {
start: { line: 2, column: 15, token: 3 },
end: { line: 2, column: 16, token: 4 },
lines: Lines {
infos: [Array],
mappings: [],
cachedSourceMap: null,
cachedTabWidth: undefined,
length: 5,
name: null
},
tokens: [
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object]
],
indent: 2
}
}
*/

recast.type.builders

recast.type.builders提供各种工具用于AST结构的组装,以帮助对于AST结构修改.

定一个小小的目标:

function add(a,b) {}改成匿名函数式声明const add = function (a,b) {...}

步骤:

  1. 创建一个VariableDeclaration变量声明对象, 声明头为const, 内容为一个即将创建的VariableDeclarator对象.
  2. 创建一个VariableDeclarator,放置add.id在左边,右边是将创建的FunctionDeclaration对象
  3. 创建一个FunctionDeclaration,包含前面前所述的三个组件id,params,body. 因为是匿名函数则id设为空,params使用add.params,body使用add.body.
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
const recast = require('recast')

const code = `
function add(a,b) {
return a+b;
}
`
const ast = recast.parse(code);
const add = ast.program.body[0]

// 引入变量声明,变量符号,函数声明三种“模具”
const {variableDeclaration, variableDeclarator, functionExpression} = recast.types.builders

// 将准备好的组件置入模具,并组装回原来的ast对象。
ast.program.body[0] = variableDeclaration("const", [
variableDeclarator(add.id, functionExpression(
null, // Anonymize the function expression.
add.params,
add.body
))
]);

//将AST对象重新转回可以阅读的代码
const output = recast.print(ast).code;

console.log(output)
/*
const add = function(a, b) {
return a+b;
};
*/

recast 高级用法

除了parse/print/builder,Recaset的三项主要功能:

  • recast.run: 通过命令行读取js文件, 并转化成AST以供处理.
  • recast.types.namedTypes: 通过assert()check(),可以验证AST对象的类型.
  • recast.visit: 遍历AST树, 获取有效的AST对象并进行更改.

recast.run

demo.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function add(a, b) {
return a + b
}

function sub(a, b) {
return a - b
}

function commonDivision(a, b) {
while (b !== 0) {
if (a > b) {
a = sub(a, b)
} else {
b = sub(b, a)
}
}
return a
}

read.js

1
2
3
4
const recast = require('recast')
recast.run( function(ast, printSource){
printSource(ast)
})

命令行输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
node read demo.js

// function add(a, b) {
// return a + b
//}
//
//function sub(a, b) {
// return a - b
//}
//
//function commonDivision(a, b) {
// while (b !== 0) {
// if (a > b) {
// a = sub(a, b)
// } else {
// b = sub(b, a)
// }
// }
// return a
//}%

recast.run读取demo.js文件,并将内容转成了AST对象.

同时它还提供了一个printSource函数,随时可以将ast的内容转换回源码,以方便调试。

recast.visit

visit.js

1
2
3
4
5
6
7
8
9
10
const recast  = require('recast')

recast.run(function(ast, printSource) {
recast.visit(ast, {
visitExpressionStatement: function({node}) {
console.log(node)
return false
}
});
});

recast.visit将AST对象内的节点进行逐个遍历

注意:

  1. 你想操作函数声明,就使用visitFunctionDelaration遍历,想操作赋值表达式,就使用visitExpressionStatement.只要在AST对象文档中的定义对象,在前面增加visit就可遍历
  2. 通过node可以取到AST对象
  3. 每个遍历函数后必须加上return false,或者选择以下写法,否则报错:
1
2
3
4
5
6
7
8
9
10
11
const recast  = require('recast')

recast.run(function(ast, printSource) {
recast.visit(ast, {
visitExpressionStatement: function(path) {
const node = path.node
printSource(node)
this.traverse(path)
}
})
});

recast.types.namedTypes

recast.types.namedTypes简称TNT,它用来判断AST对象是否为指定的类型.

TNT.Node.assert()判断Node是否规范,如果不符合规范则报错退出

TNT.Node.check()可以判断类型是否一致,并输出FalseTrue

注意: 上述Node可以替换成任意AST对象,例如TNT.ExpressionStatement.check(),TNT.FunctionDeclaration.assert()

check.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const recast = require("recast");
const TNT = recast.types.namedTypes

recast.run(function(ast, printSource) {
recast.visit(ast, {
visitExpressionStatement: function(path) {
const node = path.value
// 判断是否为ExpressionStatement,正确则输出一行字。
if(TNT.ExpressionStatement.check(node)){
console.log('这是一个ExpressionStatement')
}
this.traverse(path);
}
});
});

assert.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const recast = require("recast");
const TNT = recast.types.namedTypes

recast.run(function(ast, printSource) {
recast.visit(ast, {
visitExpressionStatement: function(path) {
const node = path.node
// 判断是否为ExpressionStatement,正确不输出,错误则全局报错
TNT.ExpressionStatement.assert(node)
this.traverse(path);
}
});
});

Babel 使用的AST 工具库

babel 就出现了,它主要解决了就是一些浏览器不兼容 Es6 新特性的问题,其实就把 Es6 代码转换为 Es5 的代码,兼容所有浏览器,babel 转换代码其实就是用了 AST,babel 与 AST 就有着很一种特别的关系。

当我们配置 babel 的时候,不管是在 .babelrc 或者 babel.config.js 文件里面配置的都有 presetsplugins 两个配置项:

插件和预设的区别

1
2
3
4
5
// .babelrc
{
"presets": ["@babel/preset-env"],
"plugins": []
}

当我们配置了 presets 中有 @babel/preset-env,那么 @babel/core 就会去找 preset-env 预设的插件包,它是一套

babel 核心包并不会去转换代码,核心包只提供一些核心 API,真正的代码转换工作由插件或者预设来完成,比如要转换箭头函数,会用到这个 plugin,@babel/plugin-transform-arrow-functions,当需要转换的要求增加时,我们不可能去一一配置相应的 plugin,这个时候就可以用到预设了,也就是 presets。presets 是 plugins 的集合,一个 presets 内部包含了很多 plugin。

babel插件的使用

使用插件集合@babel/preset-env

1
2
3
4
5
6
7
8
9
10
const babel = require('@babel/core')
const code = `const fn = (a, b) => a + b`
// babel 有 transform 方法会帮我们自动遍历,使用相应的预设或者插件转换相应的代码
const r = babel.transform(code, {
presets: ['@babel/preset-env'],
})
console.log(r.code)
// 打印结果如下
// "use strict";
// var fn = function fn(a,b) { return a + b; };

使用单独的差距@babel/plugin-transform-arrow-functions

1
2
3
4
5
6
7
8
9
const babel = require('@babel/core')
const code = `const fn = (a, b) => a + b`
// babel 有 transform 方法会帮我们自动遍历,使用相应的预设或者插件转换相应的代码
const r = babel.transform(code, {
plugins: ['@babel/plugin-transform-arrow-functions'],
})
console.log(r.code)
// 打印结果如下
// const fn = function (a,b) { return a + b; };

我们可以从打印结果发现此时并没有转换我们变量的声明方式还是 const 声明,只是转换了箭头函数

babel解析AST

babel使用的AST引擎是babylon,babylon并非babel团队自己开发,而是forkacorn项目.

如果我们使用babel处理AST对象,只需要使用:

  • @babel/parser: 将js代码 —> AST对象
  • @babel/traverse: 对AST节点进行递归遍历
  • @babel/types: 对具体的AST节点进行修改
  • @babel/generator: AST对象 —>新的js代码.

babel解析过程分为3个阶段:

  • 第1步 解析(Parse)通过解析器babylon将代码解析成抽象语法树

  • 第2步 转换(TransForm)通过babel-traverse plugin对抽象语法树进行深度优先遍历,遇到需要转换的,就直接在AST对象上对节点进行添加、更新及移除操作,比如遇到箭头函数,就转换成普通函数,最后得到新的AST树。

  • 第3步 生成(Generate)通过babel-generator将AST树生成es5代码

例子🌰

简单的跑下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");

function compile(code) {
// 1.parse 将代码解析为抽象语法树(AST)
const ast = parser.parse(code);

// 2,traverse 转换代码
traverse.default(ast, {});

// 3. generator 将 AST 转回成代码
return generator.default(ast, {}, code);
}

const code = `
function getData() {
console.log("data")
}
`;
const newCode = compile(code)
console.log(newCode)

高级点例子🌰

自动给console.log增加函数名

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");

function compile(code) {
// 1.parse
const ast = parser.parse(code);

// 2,traverse
const visitor = {
CallExpression(path) {
// 拿到 callee 数据
const { callee } = path.node;
// 判断是否是调用了 console.log 方法
// 1. 判断是否是成员表达式节点,上面截图有详细介绍
// 2. 判断是否是 console 对象
// 3. 判断对象的属性是否是 log
const isConsoleLog =
types.isMemberExpression(callee) &&
callee.object.name === "console" &&
callee.property.name === "log";
if (isConsoleLog) {
// 如果是 console.log 的调用 找到上一个父节点是函数
const funcPath = path.findParent(p => {
return p.isFunctionDeclaration();
});
// 取函数的名称
const funcName = funcPath.node.id.name;
// 将名称通过 types 来放到函数的参数前面去
path.node.arguments.unshift(types.stringLiteral(funcName));
}
}
};
// traverse 转换代码
traverse.default(ast, visitor);

// 3. generator 将 AST 转回成代码
return generator.default(ast, {}, code);
}

const code = `
function getData() {
console.log("data")
}
`;
const newCode = compile(code)
console.log(newCode)

Vue模版编译过程

Vue提供了2个版本,一个是Runtime+Compiler,另一个是Runtime only.

Runtime+Compiler是包含编译代码的,会把编译的过程放到运行时做.

Runtime only是不包含编译代码的,需要借助wenpack的vue-loader把模版编译成render函数.

无论是那个版本,都会有一个环节将模版编译成render函数.

第一步 解析(Parse)

1
const ast = parse(template.trim(), options)

将模板字符串解析生成 AST,这里的解析器是vue自己实现的,解析过程中会使用正则表达式对模板顺序解析,当解析到开始标签、闭合标签、文本的时候都会有相对应的回调函数执行,来达到构造 AST 树的目的。

生成的AST 元素节点总共有 3 种类型,1 为普通元素, 2 为表达式,3为纯文本。

例子🌰

1
2
3
<ul :class="bindCls" class="list" v-if="isShow">
<li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>

上面👆模版解析成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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
ast = {
'type': 1,
'tag': 'ul',
'attrsList': [],
'attrsMap': {
':class': 'bindCls',
'class': 'list',
'v-if': 'isShow'
},
'if': 'isShow',
'ifConditions': [{
'exp': 'isShow',
'block': // ul ast element
}],
'parent': undefined,
'plain': false,
'staticClass': 'list',
'classBinding': 'bindCls',
'children': [{
'type': 1,
'tag': 'li',
'attrsList': [{
'name': '@click',
'value': 'clickItem(index)'
}],
'attrsMap': {
'@click': 'clickItem(index)',
'v-for': '(item,index) in data'
},
'parent': // ul ast element
'plain': false,
'events': {
'click': {
'value': 'clickItem(index)'
}
},
'hasBindings': true,
'for': 'data',
'alias': 'item',
'iterator1': 'index',
'children': [
'type': 2,
'expression': '_s(item)+":"+_s(index)'
'text': '{{item}}:{{index}}',
'tokens': [
{'@binding':'item'},
':',
{'@binding':'index'}
]
]
}]
}

第二步 优化语法树(Optimize)

1
optimize(ast, options)

Vue模版中并不是所有数据都是响应式的,有很多数据在首次渲染后就永远不会改变了. 那么这部分数据生成的DOM也不会改变, 我们可以在patch的过程跳过他们的对比.

此阶段会深度遍历生成的AST树,检测它的每一颗子树是不是静态节点,如果是静态节点则它们生成DOM永远不会改变,这对运行时对模版的更新起到极大的优化作用.

遍历过程中,会对这个AST树中的每一个AST元素节点标记static和staticRoot(递归该节点的所有children,一旦子节点有不是static的情况,则为false,否则为true).

经过该阶段,上面例子中的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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
ast = {
'type': 1,
'tag': 'ul',
'attrsList': [],
'attrsMap': {
':class': 'bindCls',
'class': 'list',
'v-if': 'isShow'
},
'if': 'isShow',
'ifConditions': [{
'exp': 'isShow',
'block': // ul ast element
}],
'parent': undefined,
'plain': false,
'staticClass': 'list',
'classBinding': 'bindCls',
'static': false,
'staticRoot': false,
'children': [{
'type': 1,
'tag': 'li',
'attrsList': [{
'name': '@click',
'value': 'clickItem(index)'
}],
'attrsMap': {
'@click': 'clickItem(index)',
'v-for': '(item,index) in data'
},
'parent': // ul ast element
'plain': false,
'events': {
'click': {
'value': 'clickItem(index)'
}
},
'hasBindings': true,
'for': 'data',
'alias': 'item',
'iterator1': 'index',
'static': false,
'staticRoot': false,
'children': [
'type': 2,
'expression': '_s(item)+":"+_s(index)'
'text': '{{item}}:{{index}}',
'tokens': [
{'@binding':'item'},
':',
{'@binding':'index'}
],
'static': false
]
}]
}

第三步 生成代码(generate)

1
const code = generate(ast, options)

通过generate方法,将AST生成render函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
with(this){
return (isShow) ?
_c('ul', {
staticClass: "list",
class: bindCls
},
_l((data), function(item, index) {
return _c('li', {
on: {
"click": function($event) {
clickItem(index)
}
}
},
[_v(_s(item) + ":" + _s(index))])
})
) : _e()
}

AST对象文档

node类型全集:

1
(parameter) node: Identifier | SimpleLiteral | RegExpLiteral | Program | FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | SwitchCase | CatchClause | VariableDeclarator | ExpressionStatement | BlockStatement | EmptyStatement | DebuggerStatement | WithStatement | ReturnStatement | LabeledStatement | BreakStatement | ContinueStatement | IfStatement | SwitchStatement | ThrowStatement | TryStatement | WhileStatement | DoWhileStatement | ForStatement | ForInStatement | ForOfStatement | VariableDeclaration | ClassDeclaration | ThisExpression | ArrayExpression | ObjectExpression | YieldExpression | UnaryExpression | UpdateExpression | BinaryExpression | AssignmentExpression | LogicalExpression | MemberExpression | ConditionalExpression | SimpleCallExpression | NewExpression | SequenceExpression | TemplateLiteral | TaggedTemplateExpression | ClassExpression | MetaProperty | AwaitExpression | Property | AssignmentProperty | Super | TemplateElement | SpreadElement | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern | ClassBody | MethodDefinition | ImportDeclaration | ExportNamedDeclaration | ExportDefaultDeclaration | ExportAllDeclaration | ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier

https://github.com/babel/babylon/blob/master/ast/spec.md

参考

http://caibaojian.com/ast.html

https://juejin.cn/post/6844904035271573511

https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Parser_API#Node_objects

https://segmentfault.com/a/1190000016231512

https://my.oschina.net/fileoptions/blog/1647448

https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/726217/

https://www.lagou.com/lgeduarticle/82247.html

http://www.alloyteam.com/2017/04/analysis-of-babel-babel-overview/