什么是AST(抽象语法树)?
It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code.
AST是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
AST是一个非常基础但是同时非常重要的知识点,我们熟知的 TypeScript、babel、webpack、vue-cli 都是依赖 AST 进行开发的。
这里我们就以 babel 为例来实践一下 AST。
Babel运行原理
Babel 作为当今最为常用的 JavaScript 编译器,在前端开发中扮演着极为重要的角色。大多数情况下,Babel 被用来转译 ECMAScript 2015+ 至可兼容浏览器的版本。
Babel 的三个主要处理步骤分别是:
- 解析(parse)
- 转换(transform)
- 生成(generate)
整个过程中,parsing和generation是固定不变的,最关键的是transforming步骤,通过babel插件来支持,这是其扩展性的关键。
这三个阶段分别由 @babel/parser、@babel/core、@babel/generator 执行。Babel 本质上只是一个代码的搬运工,如果不给 Babel 装上插件,它将会把输入的代码原封不动地输出。正是因为有插件的存在, Babel 才能将输入的代码进行转变,从而生成新的代码。
解析
输入JS源码,输出AST
parsing(解析),对应于编译器的词法分析,及语法分析阶段。输入的源码字符序列经过词法分析,生成具有词法意义的token序列(能够区分出关键字、数值、标点符号等),接着经过语法分析,生成具有语法意义的AST(能够区分出语句块、注释、变量声明、函数参数等)。
利用 @babel/parser 对源代码进行解析 得到 AST。
栗如:
1 | console.log(info) |
经过parsing后,生成的AST如下:
1 | { |
🔥Tip: JS代码对应的AST结构可以通过AST Explorer工具查看
仔细的小伙伴可能就会发现从我们的源代码到AST的过程其实就是一个分词的过程,将我们的 console.log(info) 分成 console、log、info。
有了这个 AST 树结构,我们就能进行语义层面转换了。
转换
输入AST,输出修改过的AST
利用 @babel/traverse 对 AST 进行遍历,并解析出整个树的 path,通过挂载的 metadataVisitor 读取对应的元信息,这一步叫 set AST 过程。
@babel/traverse 是一款用来自动遍历抽象语法树的工具,它会访问树中的所有节点,在进入每个节点时触发 enter 钩子函数,退出每个节点时触发 exit 钩子函数。开发者可在钩子函数中对 AST 进行修改。
1 | import traverse from "@babel/traverse"; |
transforming(转换),对应于编译器的机器无关代码优化阶段(稍微有点牵强,但二者工作内容都是修改AST),对 AST 做一些修改,比如针对上面的 log 增加一些信息方便我们调试:
1 | console.log(info) => console.log('[info]', info) |
修改过后的 AST 结构:
1 | { |
语义层面的转换具体而言就是对AST进行增、删、改操作,修改后的AST可能具有不同的语义,映射回代码字符串也不同
生成
输入AST,输出JS源码
generation(生成),对应于编译器的代码生成阶段,把AST映射回代码字符串。
利用 @babel/generator 将 AST 树输出为转码后的代码字符串。
实践
说了这么多接下来我们就用代码实践一下上面的例子
相关npm包
- @babel/parser 解析输入源码,创建AST
- @babel/traverse 遍历操作AST
- @babel/generator 把AST转回JS代码
- @babel/types AST操作工具库
代码
1 | const parser = require('@babel/parser'); |
总结
看到这,我们的 AST 实践也告一段落了。当然,文章所讲的只是一个简单的例子,但基本的原理思路八九不离十,更多的类型还得自己去探究。总之,掌握好 AST,你真的可以做很多事情。