抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

TODO:还有这篇 字节分享
几乎完全转自 AST 团队分享 ,可以理解为 读后 & 自己敲一遍 的笔记

What & Why

  • What: 抽象语法树(Abstract Syntax Tree,简称 AST)是源代码的抽象语法结构的树状表现形式
  • Why:
    • webpack、eslint 等很多工具库的核心都是通过抽象语法树来实现对代码的检查、分析等操作
    • 浏览器就是通过将 js 代码转化为抽象语法树来进行下一步的分析等其他操作,所以将 js 转化为抽象语法树更利于程序的分析
  • 一个简单的例子:
    • 首先一段代码转换成的抽象语法树是一个对象,该对象会有一个顶级的 type 属性 Program;第二个属性是 body 是一个数组
    • body 数组中存放的每一项都是一个对象,里面包含了所有的对于该语句的描述信息
1
2
3
4
5
6
7
8
9
10
11
type: 描述该语句的类型  --> 变量声明的语句
kind: 变量声明的关键字 --> var
declaration: 声明内容的数组,里面每一项也是一个对象
type: 描述该语句的类型
id: 描述变量名称的对象
type: 定义
name: 变量的名字
init: 初始化变量值的对象
type: 类型
value: 值 "is tree" 不带引号
row: "\"is tree"\" 带引号

词法分析和语法分析

JavaScript解释型语言,一般通过 词法分析 -> 语法分析 -> 语法树,就可以开始解释执行了。从 code 分析成 AST ,我们可以在这里看到效果

  • 词法分析:也叫扫描 (scans),是将字符流转换为记号流 (tokens),它会读取我们的代码然后按照一定的规则合成一个个的标识 (scans: code => tokens)
    。当词法分析源代码的时候,它会一个一个字符的读取代码;当它遇到空格、操作符,或者特殊符号的时候,它会认为一个话已经完成了。比如说:var a = 2 ,这段代码通常会被分解成 var、a、=、2
1
2
3
4
5
6
;[
{ type: 'Keyword', value: 'var' },
{ type: 'Identifier', value: 'a' },
{ type: 'Punctuator', value: '=' },
{ type: 'Numeric', value: '2' },
]
  • 语法分析:也称解析器,将词法分析出来的数组转换成树的形式,同时验证语法。语法如果有错的话,抛出语法错误 (analyze: tokens => syntax)
1
2
3
4
5
6
7
8
9
{
...
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
...
}
  • syntax => tree

What can AST do

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

eg. 有个函数 function a() {} 我想把它变成 function b() {}
eg. 在 webpack 中代码编译完成后 require('a') –> __webapck__require__("*/**/a.js")

The analysis process of AST

How to analysis

先说结果:AST 是深度优先遍历
比如说一段代码 function getUser() {},我们把函数名字更改为 hello
推荐另一个常用的 AST 在线转换网站ast explorer;可以在网站看转换效果,也可以用下面的工具看效果

  • 工具 (这三个是操作 AST 的三个重要模块,也是实现 babel 的核心依赖)
    • esprima:code => ast
    • estraverse: traverse ast
    • escodegen: ast => code
  • 实例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const esprima = require('esprima')
const estraverse = require('estraverse')
const code = `function getUser() {}`
// 生成 AST
const ast = esprima.parseScript(code)
// 转换 AST,只会遍历 type 属性
// traverse 方法中有进入和离开两个钩子函数
estraverse.traverse(ast, {
enter(node) {
console.log('enter -> node.type', node.type)
},
leave(node) {
console.log('leave -> node.type', node.type)
},
})
  • 打印结果
  • 所以是深度优先遍历

Demo: 修改函数名字

此时我们发现函数的名字在 typeIdentifier 的时候就是该函数的名字,我们就可以直接修改它便可实现一个更改函数名字的 AST 工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 转换树
estraverse.traverse(ast, {
// 进入离开修改都是可以的
enter(node) {
console.log('enter -> node.type', node.type)
if (node.type === 'Identifier') {
node.name = 'hello'
}
},
leave(node) {
console.log('leave -> node.type', node.type)
},
})
// 生成新的代码
const result = escodegen.generate(ast)
console.log(result)
// function hello() {}

Application of AST

babel

The working mechanism of babel

babel 的主要作用就是 Es6 代码转换为 Es5 的代码,以兼容所有浏览器,主要原理就是运用了 AST。有个 class 语法转为 ES5 语法的 demo,见博客 AST 抽象语法树

babel两个工具包重要的包: @babel/core (打通webpack和babel的一个通道)、@babel/preset-env (翻译器) (还有一个@babel/polyfill(添加ES6中的新函数新变量,eg. Promise和map))。当我们配置 babel 的时候,不管是在 .babelrc 或者 babel.config.js 文件里面配置的都有 presets 和 plugins 两个配置项

presets和plugins的区别

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

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

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

babel中转换代码一个demo

现在我们有一个箭头函数,要想把它转成普通函数,我们就可以直接这么写:

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() { return a + b; };

此时我们可以看到最终代码会被转成普通函数,但是我们,只需要箭头函数转通函数的功能,不需要用这么大一套包,只需要一个箭头函数转普通函数的包,我们其实是可以在 node_modules 下面找到有个叫做 plugin-transform-arrow-functions 的插件,这个插件是专门用来处理 箭头函数的,我们就可以这么写:

1
2
3
4
5
6
const r = babel.transform(code, {
plugins: ['@babel/plugin-transform-arrow-functions'],
})
console.log(r.code)
// 打印结果如下
// const fn = function () { return a + b; };

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

编写一个箭头函数转普通函数的babel插件

首先应该看 Babel插件开发入门指南
现在我们来个实战把 const fn = (a, b) => a + b 转换为 const fn = function(a, b) { return a + b }

分析 AST 结构

首先我们在在线分析 AST 的网站上分析 const fn = (a, b) => a + bconst fn = function(a, b) { return a + b } 看两者语法树的区别

分析可得:

  • 变成普通函数之后他就不叫箭头函数了 ArrowFunctionExpression,而是函数表达式了 FunctionExpression
  • 所以首先我们要把 箭头函数表达式(ArrowFunctionExpression) 转换为 函数表达式(FunctionExpression)
  • 要把 二进制表达式(BinaryExpression) 放到一个 代码块中(BlockStatement)的 ExpressionStatement/ ReturnStatement 中。
  • 其实我们要做就是把一棵树变成另外一颗树,说白了其实就是拼成另一颗树的结构,然后生成新的代码,就可以完成代码的转换

访问者模式

在 babel 中,我们开发 plugins 的时候要用到访问者模式,就是说在访问到某一个路径的时候进行匹配,然后在对这个节点进行修改。这里的节点指的是AST树中的节点,比如下面是访问到 ArrowFunctionExpression 节点就执行下面的 ArrowFunctionExpression() 函数

那么我们就可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const babel = require('@babel/core')
const code = `const fn = (a, b) => a + b` // 转换后 const fn = function(a, b) { return a + b }
const arrowFnPlugin = {
// 访问者模式
visitor: {
// 当访问到某个路径的时候进行匹配,比如下面是访问到 ArrowFunctionExpression 就执行下面的 ArrowFunctionExpression() 函数
ArrowFunctionExpression(path) {
// 拿到节点
const node = path.node
console.log('ArrowFunctionExpression -> node', node)
},
},
}

const r = babel.transform(code, {
plugins: [arrowFnPlugin],
})

console.log(r)

修改 AST 结构

此时我们拿到的结果是这样的节点结果是 这样的,其实就是 ArrowFunctionExpression 的 AST,此时我们要做的是把 ArrowFunctionExpression 的结构替换成 FunctionExpression的结构。但要我们自己手写替换得毫无差错是很麻烦的,所以 babel 为我们提供了一个神仙工具叫做 @babel/types 中文版

@babel/types 有两个作用:

  • 判断当前节点是不是某种特定节点(eg. 相当于之前的 node.type === ‘ArrowFunctionExpression’ )
  • 生成对应的表达式

那么接下来我们就开始生成一个 FunctionExpression,然后把之前的 ArrowFunctionExpression 替换掉,我们可以看 @babel/types 文档,找到 functionExpression 方法,该方法接受相应的参数我们传递过去即可生成一个 FunctionExpression

t.functionExpression(id, params, body, generator, async)

  • id: Identifier (default: null) id 可传递 null
  • params: Array (required) 函数参数,可以把之前的参数拿过来
  • body: BlockStatement (required) 函数体,接受一个 BlockStatement 我们需要生成一个
  • generator: boolean (default: false) 是否为 generator 函数,当然不是了
  • async: boolean (default: false) 是否为 async 函数,肯定不是了

还需要生成一个 BlockStatement,我们接着看文档找到 BlockStatement 接受的参数

t.blockStatement(body, directives)

看文档说明,blockStatement 接受一个 body,那我们把之前的 body 拿过来就可以直接用,不过这里 body 接受一个数组

我们细看 AST 结构,函数表达式中的 BlockStatement 中的 body 是一个 ReturnStatement,所以我们还需要生成一个 ReturnStatement

现在我们就可以改写 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
const babel = require('@babel/core')
const t = require('@babel/types')
const code = `const fn = (a, b) => a + b` // const fn = function(a, b) { return a + b }
const arrowFnPlugin = {
// 访问者模式
visitor: {
// 当访问到某个路径的时候进行匹配
ArrowFunctionExpression(path) {
// 拿到节点然后替换节点
const node = path.node
console.log('ArrowFunctionExpression -> node', node)
// 拿到函数的参数
const params = node.params
const body = node.body
const functionExpression = t.functionExpression(
null,
params,
t.blockStatement([body])
)
// 替换原来的函数
path.replaceWith(functionExpression)
},
},
}
const r = babel.transform(code, {
plugins: [arrowFnPlugin],
})
console.log(r.code) // const fn = function (a, b) { return a + b; };

特殊情况

我们知道在剪头函数中是可以省略 return 关键字,我们上面是处理了省略关键字的写法,但是如果用户写了 return 关键字后,我们写的这个插件就有问题了,所以我们可以在优化一下

1
const fn = (a, b) => { retrun a + b } -> const fn = function(a, b) { return a + b }

观察代码我们发现,我们就不需要把 body 转换成 blockStatement 了,直接放过去就可以了,那么我们就可以这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ArrowFunctionExpression(path) {
// 拿到节点然后替换节点
const node = path.node
console.log("ArrowFunctionExpression -> node", node)
// 拿到函数的参数
const params = node.params
let body = node.body
// 判断是不是 blockStatement,不是的话让他变成 blockStatement
if (!t.isBlockStatement(body)) {
body = t.blockStatement([body])
}
const functionExpression = t.functionExpression(null, params, body)
// 替换原来的函数
path.replaceWith(functionExpression)
}

编写一个claas转ES5的babel插件

AST 抽象语法树
类似的实际插件是 @babel/plugin-transform-classes

按需引入

我们用 element-ui 或 antd 的时候,样式都支持全局引入和按需引入。按需引入需要安装一个 babel-plugin-import 的插件,将全局的写法变成按需引入的写法。其实也是用了AST语法树转换的原理,实现

1
2
3
4
import { Button } from 'antd';
↓ ↓ ↓ ↓ ↓ ↓
var _button = require('antd/lib/button');
require('antd/lib/button/style');

(这个插件详见另一篇博客 按需加载 & 样式自动加载 —— babel-plugin-import)

babylon

在 babel 官网上有一句话 Babylon is a JavaScript parser used in Babel。(即是Babel 的解析引擎)

babel 与 babylon 的关系

from alloyteam-剖析babel
Babel 使用的引擎是 babylon,babylon 并非由 babel 团队自己开发的,而是 fork 的 acorn 项目,acorn 的项目本人在很早之前在兴趣部落 1.0 在构建中使用,为了是做一些代码的转换,是很不错的一款引擎,不过 acorn 引擎只提供基本的解析 ast 的能力,遍历还需要配套的 acorn-travesal, 替换节点需要使用 acorn-,而这些开发,在 Babel 的插件体系开发下,变得一体化了

使用 babylon 编写一个数组扩展运算符 转 Es5 语法的插件

const arr = [ ...arr1, ...arr2 ] 转成 var arr = [].concat(arr1, arr2)

我们使用 babylon 的话就不需要使用 @babel/core 了,只需要用到他里面的 traversegenerator,用到的包有 babylon@babel/traverse@babel/generator@babel/types

分析语法树

看一下两棵语法树的区别

根据上图我们分析得出:

  • 两棵树都是变量声明的方式,不同的是他们声明的关键字不一样
  • 他们初始化变量值的时候是不一样的,一个数组表达式(ArrayExpression)另一个是调用表达式(CallExpression)
  • 那我们要做的就很简单了,就是把 数组表达式转换为调用表达式就可以

分析类型

这段代码的核心生成一个 callExpression 调用表达式,所以对应官网上的类型,我们分析需要用到的 api

  • 先来分析 init 里面的,首先是 callExpression
    1
    2
    3
    4
    5
    /**
    * @param {Expression} callee (required)
    * @param {Array<Expression | SpreadElement | JSXNamespacedName>} source (required)
    */
    t.callExpression(callee, arguments)
  • 对应语法树上 callee 是一个 MemberExpression,所以要生成一个成员表达式
    1
    2
    3
    4
    5
    6
    7
    /**
    * @param {Expression} object (required)
    * @param {if computed then Expression else Identifier} property (required)
    * @param {boolean} computed (default: false)
    * @param {boolean} optional (default: null)
    */
    t.memberExpression(object, property, computed, optional)
  • 在 callee 的 object 是一个 ArrayExpression 数组表达式,是一个空数组
    1
    2
    3
    4
    /**
    * @param {Array<null | Expression | SpreadElement>} elements (default: [])
    */
    t.arrayExpression(elements)
  • 对了里面的东西分析完了,我们还要生成 VariableDeclarator 和 VariableDeclaration 最终生成新的语法树
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * @param {LVal} id (required)
    * @param {Expression} init (default: null)
    */
    t.variableDeclarator(id, init)

    /**
    * @param {"var" | "let" | "const"} kind (required)
    * @param {Array<VariableDeclarator>} declarations (required)
    */
    t.variableDeclaration(kind, declarations)
    其实倒着分析语法树,分析完怎么写也就清晰了,那么我们开始上代码吧

上代码

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 babylon = require('babylon')
// 使用 babel 提供的包,traverse 和 generator 都是被暴露在 default 对象上的
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')

const code = `const arr = [ ...arr1, ...arr2 ]` // var arr = [].concat(arr1, arr2)

const ast = babylon.parse(code, {
sourceType: 'module',
})

// 转换树
traverse(ast, {
VariableDeclaration(path) {
const node = path.node
const declarations = node.declarations
console.log('VariableDeclarator -> declarations', declarations)
const kind = 'var'
// 边界判定
if (node.kind !== kind && declarations.length === 1 && t.isArrayExpression(declarations[0].init)) {
// 取得之前的 elements
const args = declarations[0].init.elements.map((item) => item.argument)
const callee = t.memberExpression(t.arrayExpression(), t.identifier('concat'), false)
const init = t.callExpression(callee, args)
const declaration = t.variableDeclarator(declarations[0].id, init)
const variableDeclaration = t.variableDeclaration(kind, [declaration])
path.replaceWith(variableDeclaration)
}
},
})

具体语法书

和抽象语法树相对的是具体语法树(Concrete Syntax Tree)简称 CST(通常称作分析树)。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树。一旦 AST 被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。可参考抽象语法树和具体语法树有什么区别?

补充

关于 node 类型,全集大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
(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

Babel 有文档对 AST 树的详细定义,可参考这里

参考资料
Babel插件开发入门指南
AST 团队分享
alloyteam-剖析babel
JavaScript 语法解析、AST、V8、JIT
详解AST抽象语法树

评论