AST懵逼入门
文章目录
AST(Abstract Syntax Tree)抽象语法树,或简称语法树(Syntax tree). 是源代码语法结构的一种抽象表示. 它以树状的形式表现编程语言的语法结构, 树上的每一个节点都表示源代码中的一种结构.
1 | while b ≠ 0 |
之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于 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 | [ |
在线分词工具:esprima/parser
语法分析
分词后得到一个个独立的语法单元, 这些语法单元需要建立其实际的关系才有意义. 建立语法单元的这个过程就是语法分析,这个一般是递归过程.
(1+2)*3;
最后的语法分析结果是:
1 | { |
astexplorer 在线可视化 AST工具.
body
表示程序体,而程序体中包含了一则表达式ExpressionStatement
, 表达式体里包含了操作符 *
,以及左右两边表达式,其中右边是数字3
,而左边表达式还包含一层表达式,里面是一个+
操作符,以及左右两边分别为1
和2
的数字。
recast - 操作AST瑞士军刀
recast
可以将 JS代码块解析成 AST,并针对AST进行JS代码的修改.
1 | const recast = require('recast') |
recast.type.builders
recast.type.builders
提供各种工具用于AST结构的组装,以帮助对于AST结构修改.
定一个小小的目标:
function add(a,b) {}
改成匿名函数式声明const add = function (a,b) {...}
步骤:
- 创建一个
VariableDeclaration
变量声明对象, 声明头为const
, 内容为一个即将创建的VariableDeclarator
对象. - 创建一个
VariableDeclarator
,放置add.id
在左边,右边是将创建的FunctionDeclaration
对象 - 创建一个
FunctionDeclaration
,包含前面前所述的三个组件id
,params
,body
. 因为是匿名函数则id设为空,params
使用add.params
,body
使用add.body
.
1 | const recast = require('recast') |
recast 高级用法
除了parse/print/builder
,Recaset的三项主要功能:
recast.run
: 通过命令行读取js文件, 并转化成AST以供处理.recast.types.namedTypes
: 通过assert()
和check()
,可以验证AST对象的类型.recast.visit
: 遍历AST树, 获取有效的AST对象并进行更改.
recast.run
demo.js
1 | function add(a, b) { |
read.js
1 | const recast = require('recast') |
命令行输入:
1 | node read demo.js |
recast.run
读取demo.js文件,并将内容转成了AST对象.
同时它还提供了一个printSource
函数,随时可以将ast的内容转换回源码,以方便调试。
recast.visit
visit.js
1 | const recast = require('recast') |
recast.visit将AST对象内的节点进行逐个遍历
注意:
- 你想操作函数声明,就使用
visitFunctionDelaration
遍历,想操作赋值表达式,就使用visitExpressionStatement
.只要在AST对象文档中的定义对象,在前面增加visit
就可遍历 - 通过
node
可以取到AST对象 - 每个遍历函数后必须加上return false,或者选择以下写法,否则报错:
1 | const recast = require('recast') |
recast.types.namedTypes
recast.types.namedTypes
简称TNT
,它用来判断AST对象是否为指定的类型.
TNT.Node.assert()
判断Node
是否规范,如果不符合规范则报错退出
TNT.Node.check()
可以判断类型是否一致,并输出False
和True
注意: 上述Node可以替换成任意AST对象,例如TNT.ExpressionStatement.check(),TNT.FunctionDeclaration.assert()
check.js
1 | const recast = require("recast"); |
assert.js
1 | const recast = require("recast"); |
Babel 使用的AST 工具库
babel 就出现了,它主要解决了就是一些浏览器不兼容 Es6 新特性的问题,其实就把 Es6 代码转换为 Es5 的代码,兼容所有浏览器,babel 转换代码其实就是用了 AST,babel 与 AST 就有着很一种特别的关系。
当我们配置 babel 的时候,不管是在 .babelrc
或者 babel.config.js
文件里面配置的都有 presets
和 plugins
两个配置项:
插件和预设的区别
1 | // .babelrc |
当我们配置了 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 | const babel = require('@babel/core') |
使用单独的差距@babel/plugin-transform-arrow-functions
1 | const babel = require('@babel/core') |
我们可以从打印结果发现此时并没有转换我们变量的声明方式还是 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 | const generator = require("@babel/generator"); |
高级点例子🌰
自动给console.log
增加函数名
1 | const generator = require("@babel/generator"); |
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 | <ul :class="bindCls" class="list" v-if="isShow"> |
上面👆模版解析成AST对象如下:
1 | ast = { |
第二步 优化语法树(Optimize)
1 | optimize(ast, options) |
Vue模版中并不是所有数据都是响应式的,有很多数据在首次渲染后就永远不会改变了. 那么这部分数据生成的DOM也不会改变, 我们可以在patch的过程跳过他们的对比.
此阶段会深度遍历生成的AST树,检测它的每一颗子树是不是静态节点,如果是静态节点则它们生成DOM永远不会改变,这对运行时对模版的更新起到极大的优化作用.
遍历过程中,会对这个AST树中的每一个AST元素节点标记static和staticRoot(递归该节点的所有children,一旦子节点有不是static的情况,则为false,否则为true).
经过该阶段,上面例子中的AST会变成
1 | ast = { |
第三步 生成代码(generate)
1 | const code = generate(ast, options) |
通过generate
方法,将AST生成render函数
1 | with(this){ |
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/
作者: Fynn
链接: https://fynn90.github.io/2020/08/21/AST%E6%87%B5%E9%80%BC%E5%85%A5%E9%97%A8/
本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可