TODO:还有这篇 字节分享
几乎完全转自 AST 团队分享 ,可以理解为 读后 & 自己敲一遍 的笔记
What & Why
- What: 抽象语法树(Abstract Syntax Tree,简称 AST)是源代码的抽象语法结构的树状表现形式
- Why:
- webpack、eslint 等很多工具库的核心都是通过抽象语法树来实现对代码的检查、分析等操作
- 浏览器就是通过将 js 代码转化为抽象语法树来进行下一步的分析等其他操作,所以将 js 转化为抽象语法树更利于程序的分析
- 一个简单的例子:
- 首先一段代码转换成的抽象语法树是一个对象,该对象会有一个顶级的 type 属性 Program;第二个属性是 body 是一个数组
- body 数组中存放的每一项都是一个对象,里面包含了所有的对于该语句的描述信息
1 | type: 描述该语句的类型 --> 变量声明的语句 |
词法分析和语法分析
JavaScript
是解释型语言,一般通过 词法分析 -> 语法分析 -> 语法树
,就可以开始解释执行了。从 code 分析成 AST ,我们可以在这里看到效果
- 词法分析:也叫扫描 (scans),是将字符流转换为记号流 (tokens),它会读取我们的代码然后按照一定的规则合成一个个的标识 (scans: code => tokens)
。当词法分析源代码的时候,它会一个一个字符的读取代码;当它遇到空格、操作符,或者特殊符号的时候,它会认为一个话已经完成了。比如说:var a = 2 ,这段代码通常会被分解成 var、a、=、2
1 | ;[ |
- 语法分析:也称解析器,将词法分析出来的数组转换成树的形式,同时验证语法。语法如果有错的话,抛出语法错误 (analyze: tokens => syntax)
1 | { |
- 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 | const esprima = require('esprima') |
- 打印结果
- 所以是深度优先遍历
Demo: 修改函数名字
此时我们发现函数的名字在 type
为 Identifier
的时候就是该函数的名字,我们就可以直接修改它便可实现一个更改函数名字的 AST 工具
1 | // 转换树 |
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 | // .babelrc |
当我们配置了 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 | const babel = require('@babel/core') |
此时我们可以看到最终代码会被转成普通函数,但是我们,只需要箭头函数转通函数的功能,不需要用这么大一套包,只需要一个箭头函数转普通函数的包,我们其实是可以在 node_modules
下面找到有个叫做 plugin-transform-arrow-functions
的插件,这个插件是专门用来处理 箭头函数的,我们就可以这么写:
1 | const r = babel.transform(code, { |
我们可以从打印结果发现此时并没有转换我们变量的声明方式还是 const 声明,只是转换了箭头函数
编写一个箭头函数转普通函数的babel插件
首先应该看 Babel插件开发入门指南
现在我们来个实战把 const fn = (a, b) => a + b
转换为 const fn = function(a, b) { return a + b }
分析 AST 结构
首先我们在在线分析 AST 的网站上分析 const fn = (a, b) => a + b
和 const fn = function(a, b) { return a + b }
看两者语法树的区别
分析可得:
- 变成普通函数之后他就不叫箭头函数了
ArrowFunctionExpression
,而是函数表达式了FunctionExpression
- 所以首先我们要把 箭头函数表达式(
ArrowFunctionExpression
) 转换为 函数表达式(FunctionExpression
) - 要把 二进制表达式(
BinaryExpression
) 放到一个 代码块中(BlockStatement
)的ExpressionStatement
/ReturnStatement
中。 - 其实我们要做就是把一棵树变成另外一颗树,说白了其实就是拼成另一颗树的结构,然后生成新的代码,就可以完成代码的转换
访问者模式
在 babel 中,我们开发 plugins 的时候要用到访问者模式,就是说在访问到某一个路径的时候进行匹配,然后在对这个节点进行修改。这里的节点指的是AST树中的节点,比如下面是访问到 ArrowFunctionExpression 节点就执行下面的 ArrowFunctionExpression() 函数
那么我们就可以这么写:
1 | const babel = require('@babel/core') |
修改 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 | const babel = require('@babel/core') |
特殊情况
我们知道在剪头函数中是可以省略 return 关键字,我们上面是处理了省略关键字的写法,但是如果用户写了 return 关键字后,我们写的这个插件就有问题了,所以我们可以在优化一下
1 | const fn = (a, b) => { retrun a + b } -> const fn = function(a, b) { return a + b } |
观察代码我们发现,我们就不需要把 body 转换成 blockStatement
了,直接放过去就可以了,那么我们就可以这么写
1 | ArrowFunctionExpression(path) { |
编写一个claas转ES5的babel插件
见 AST 抽象语法树
类似的实际插件是 @babel/plugin-transform-classes
按需引入
我们用 element-ui 或 antd 的时候,样式都支持全局引入和按需引入。按需引入需要安装一个 babel-plugin-import
的插件,将全局的写法变成按需引入的写法。其实也是用了AST语法树转换的原理,实现
1 | import { Button } from 'antd'; |
(这个插件详见另一篇博客 按需加载 & 样式自动加载 —— 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
了,只需要用到他里面的 traverse
和 generator
,用到的包有 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 | const babylon = require('babylon') |
具体语法书
和抽象语法树相对的是具体语法树(Concrete Syntax Tree)简称 CST(通常称作分析树)。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树。一旦 AST 被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。可参考抽象语法树和具体语法树有什么区别?
补充
关于 node 类型,全集大致如下:
1 | (parameter) node: Identifier | SimpleLiteral | RegExpLiteral | Program | FunctionDeclaration | FunctionExpression | |
Babel 有文档对 AST 树的详细定义,可参考这里
参考资料
Babel插件开发入门指南
AST 团队分享
alloyteam-剖析babel
JavaScript 语法解析、AST、V8、JIT
详解AST抽象语法树