前端开发中,使用了很多工具,譬如webpack、eslint来提升研发效率,但我们并不知道这些工具的实现原理。基于这些工具的核心都是抽象语法树,那我们就从抽象语法树开始理解底层原理的新世界吧。
一、抽象语法树是什么
顾名思义,首先可以确定的是,这是一颗跟语法相关的树。
先上一盘硬菜,维基百科定义如下:
In computer science, an abstract syntax tree (AST), or just syntax tree, is a tree representation of the abstract syntactic structure of source code written in a programming language.
也就是说,抽象语法树,是通过编程语言编写的代码的抽象语法结构。
用通俗的话讲,我们所使用的编程语言是一门对人类友好的语言,但是对于程序分析来讲,并不友好。因此,需要将编程语言转译成对程序分析友好的语言。
太干了,来点配菜。
我们结合编译过程,来说明抽象语法树的作用。
如下图所示,一般的编译过程分为六个过程。
那么在语法分析阶段,就是要将词法分析阶段得到的分词结果整合成一棵语法树。简单举个例子:
代码:
function foo(a) {
let b = a + 3;
return b;
}
将这个函数转成抽象语法树,其核心部分如下图所示:
blockstatement 块级作用域。
VariableDecalaration 变量声明
VariableDecalarator 变量声明器
Returnstatement 返回语句
在转成语法树之后,就可以对语句进行语法检查或进行修改了。
二、 语法树的应用
前面提到,可以通过对语法树分析来进行语法检查,有没有很熟悉? 有没有想到我们日常写代码过程中用到的eslint 插件、括号高亮插件等。 没有错,他们就是通过对ast 抽象语法树进行分析,来达成语法检查和高亮目的。
话不多说,看图。
这是一个ast 在我们开发流程中的一个应用总结。
编写代码:我们用 vue的 模板语法去定义一些功能,而这些模板语法最后都要通过对 ast 进行分析最终转为原生 html 和原生 js。
babel 、webpack 和 ast 的关系:
开发过程中,我们使用了大量处于提案阶段的 es6 的 语法,譬如装饰器、箭头函数、模板字符串等,而这些语法,实际上浏览器本身并不支持。因此,需要将其转成 浏览器支持的 es5 代码。这个es 降级的过程就是通过 babel 完成的,而 babel 的核心也就是通过对 ast 进行更改,从而实现 es 语法降级。
webpack 作为模块化开发的主力军,在打包阶段,也是通过对语法树进行分析,最终打包成一个文件上传到服务器上去的。
浏览器执行代码和 ast 的关系:
如图中所示,浏览器执行js 代码的过程和我们上面所阐述的编译的六个阶段是极其一致的。
综上所述,语法树和我们开发的整个过程都是息息相关的。
认识到了语法树在开发过程中的重要性。 接下来,我们通过 babel 的核心库 来简单模拟 es6 转 es5 的过程。
转译过程大致可以分为三步:
- 生成语法树
- 语法树遍历,更改
- 再次遍历语法树,生成新的代码。
通过库 esprima 生成语法树, 通过库 estraverse 遍历语法树并进行语法更改, 通过库 escodegen 遍历语法树,并生成新的代码。
三、 语法树的生成
语法树生成使用的库是 esprima。
我们先通过一个简单的es6 例子来说明。
es6 通过 let 关键字进行变量声明,从而实现块级作用域。那么转成 es 5 的话只能是 ‘var’。即,代码 ”let a = 15; “ 转变为 ”var a = 15;“。
首先,导入所使用的几个库(库的地址在本文末尾。)并定义字符串。
const prima = require('esprima');
const codegen = require('escodegen');
const traverse = require('estraverse');
var code2 = 'let a = 2';
我们知道,如果要转成语法树,首先要进行词法分析,通过调用esprima 下的 tokenize函数(console.log(prima.tokenize(code2)))可以查看其生成的token。也可以通过可视化工具esprima 可视化查看词法分析结果。
从图中可以看出,esprima 对字符串中的每一个此进行解析,并赋予相应的 type。
在词法解析结束后,再进行语法解析形成 ast 树,可以通过命令行
console.log(prima.parseScript(code2));
查看其 ast 的结果,也可以通过 ast 可视化工具 查看其结果。这里展示通过 ast 可视化工具的结果:
es6 语法树:
es5语法树:
对比这两棵语法树,我们可以看出,其唯一的区别在于,变量声明下,其 kind 节点的值。 那么,接下来,在遍历语法树的时候,我们只要对 node 下的 kind 节点进行变更就可以完成语法转变了。
四、语法树的遍历和更改
我们可以通过 estraverse下的 traverse 函数进行语法树遍历并进行相应更改。
const prima = require('esprima');
const codegen = require('escodegen');
const traverse = require('estraverse');
var code2 = 'let a = 2';
五、生成 es5 代码
我们通过 escodegen 下的 generate 函数生成 新的es5 代码:
let code2_es5 = codegen.generate(ast2);
console.log(code2_es5);
//'var a = 2;'
写到这里,基本上对 ast 的 what、why、how 进行了一个基本的介绍。 但美中不足的是,上面对于语法树的解析只是利用库函数进行的。接下来,给大家介绍一些 ast 相关的小知识,并尝试带大家,读一读这些函数的源代码。
六、AST 小知识
js 解析器有哪些?我们所使用的工具内核是什么?
常用的 js 解析器有:
esprima: https://github.com/jquery/esprima
acorn:https://github.com/acornjs/acorn
acorn 的诞生晚于 esprima,期因是 esprima 转换速度太慢。
而 babel 目前所用的解析器 fork 自 acorn。webpack 的核心 parser 也是 acorn。而 eslint 作为一个可配置的代码规范检查工具,可以任意选择定义解析器来使用。
而不管是那个解析器,他们解析得到的 ast 树都符合ast 规则:https://github.com/estree/estree
规则起源:
在v8引擎之前,最早js引擎是SpiderMonkey,第一个版本由js作者Brendan Eich设计,后交给Mozilla组织维护。js引擎在执行js文件时,都会先将js代码转换成抽象语法树(AST)。有一天,一位Mozilla工程师在FireFox中公开了这个将代码转成AST的解析器Api,也就是Parser_API,后来被人整理到github项目estree,慢慢的成了业界的规范。
七、 相关推荐
从零实现一个简易编译器:https://github.com/YongzeYao/the-super-tiny-compiler-CN
Babel 插件和 ast 转化过程:https://juejin.cn/post/6844903992762318855