(魔兽世界)Babel编译三个阶段解析:抽象语法树再生成代码字符串
概述
本文不再介绍Babel是什么也不讲怎么用,这类文章很多,我也不觉得自己能写得更好。这篇文章的关注点是另一个方面,也是很多人会好奇的事情,Babel的工作原理是什么。
Babel工作的三个阶段
首先要说明的是,现在前端流行用的WebPack或其他同类工程化工具会将源文件组合起来js中字符串分割,这部分并不是Babel完成的,是这些打包工具自己实现的,Babel的功能非常纯粹,以字符串的形式将源代码传给它,它就会返回一段新的代码字符串(以及sourcemap)。他既不会运行你的代码,也不会将多个代码打包到一起,它就是个编译器,输入语言是ES6+,编译目标语言是ES5。
在Babel官网,plugins菜单下藏着一个链接:thejameskyle/the-super-tiny-compiler。它已经解释了整个工作过程,有耐心者可以自己研究,当然也可以继续看我的文章。
Babel的编译过程跟绝大多数其他语言的编译器大致同理,分为三个阶段:
解析:将代码字符串解析成抽象语法树
变换:对抽象语法树进行变换操作
再建:根据变换后的抽象语法树再生成代码字符串
像我们在.babelrc里配置的presets和plugins都是在第2步工作的。
举个例子,首先你输入的代码如下:
<pre style="padding: 0.88889em;max-width: 100%;overflow-wrap: normal;font-size: 0.9em;word-break: normal;overflow: auto;background: rgb(246, 246, 246);border-radius: 4px;box-sizing: border-box !important;">if (1 > 0) {<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> alert('hi');<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
</pre>
经过第1步得到一个如下的对象:
<pre style="padding: 0.88889em;max-width: 100%;overflow-wrap: normal;font-size: 0.9em;word-break: normal;overflow: auto;background: rgb(246, 246, 246);border-radius: 4px;box-sizing: border-box !important;">{<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "type": "Program", // 程序根节点<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "body": [ // 一个数组包含所有程序的顶层语句<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> {<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "type": "IfStatement", // 一个if语句节点<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "test": { // if语句的判断条件<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "type": "BinaryExpression", // 一个双元运算表达式节点<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "operator": ">", // 运算表达式的运算符<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "left": { // 运算符左侧值<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "type": "Literal", // 一个常量表达式<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "value": 1 // 常量表达式的常量值<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> },<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "right": { // 运算符右侧值<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "type": "Literal",<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "value": 0<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> }<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> },<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "consequent": { // if语句条件满足时的执行内容<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "type": "BlockStatement", // 用{}包围的代码块<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "body": [ // 代码块内的语句数组<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> {<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "type": "ExpressionStatement", // 一个表达式语句节点<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "expression": {<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "type": "CallExpression", // 一个函数调用表达式节点<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "callee": { // 被调用者<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "type": "Identifier", // 一个标识符表达式节点<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "name": "alert"<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> },<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "arguments": [ // 调用参数<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> {<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "type": "Literal",<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "value": "hi"<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> }<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> ]<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> }<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> }<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> ]<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> },<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> "alternative": null // if语句条件未满足时的执行内容<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> }<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> ]<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
</pre>
Babel实际生成的语法树还会包含更多复杂信息,这里只展示比较关键的部分,欲了解更多关于ES语言抽象语法树规范可阅读:The ESTree Spec。
用图像更简单地表达上面的结构:
第1步转换的过程中可以验证语法的正确性,同时由字符串变为对象结构后更有利于精准地分析以及进行代码结构调整。
第2步原理就很简单了,就是遍历这个对象所描述的抽象语法树,遇到哪里需要做一下改变,就直接在对象上进行操作,比如我把IfStatement给改成WhileStatement就达到了把条件判断改成循环的效果。
第3步也简单,递归遍历这颗语法树,然后生成相应的代码,大概的实现逻辑如下:
<pre style="padding: 0.88889em;max-width: 100%;overflow-wrap: normal;font-size: 0.9em;word-break: normal;overflow: auto;background: rgb(246, 246, 246);border-radius: 4px;box-sizing: border-box !important;">const types = {<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> Program (node) {<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> return node.body.map(child => generate(child));<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> },<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> IfStatement (node) {<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> let code =
if (${generate(node.test)}) ${generate(node.consequent)};<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> if (node.alternative) {<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> code +=
else ${generate(node.alternative)};<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> }<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> return code;<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> },<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> BlockStatement (node) {<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> let code = node.body.map(child => generate(child));<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> code =
{ ${code} };<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> return code;<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> },<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> ......<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />function generate(node) {<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> return types[node.type](node);<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />const ast = Babel.parse(...); // 将代码解析成语法树<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />const generatedCode = generate(ast); // 将语法树重新组合成代码<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
</pre>
抽象语法树是如何产生的
第2、3步相信不用花多少篇幅大家自己都能理解,重点介绍的第一步来了。
解析这一步又分成两个步骤:
分词:将整个代码字符串分割成语法单元数组
语义分析:在分词结果的基础之上分析语法单元之间的关系
我们一步步讲。
分词
首先解释一下什么是语法单元:语法单元是被解析语法当中具备实际意义的最小单元,通俗点说就是类似于自然语言中的词语。
看这句话“2020年奥运会将在东京举行”,不论词性及主谓关系等,人第一步会把这句话拆分成:2020年、奥运会、将、在、东京、举行。这就是分词:把整句话拆分成有意义的最小颗粒,这些小块不能再被拆分,否则就失去它所能表达的意义了。
那么回到代码的解析当中,JS代码有哪些语法单元呢?大致有以下这些(其他语言也许类似但通常都有区别):
分词的过过程从逻辑来讲并不难解释,但是这是个精细活,要考虑清楚所有的情况。还是以一个代码为例:
<pre style="padding: 0.88889em;max-width: 100%;overflow-wrap: normal;font-size: 0.9em;word-break: normal;overflow: auto;background: rgb(246, 246, 246);border-radius: 4px;box-sizing: border-box !important;">if (1 > 0) {<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /> alert("if \"1 > 0\"");<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}
</pre>
我们希望得到的分词是:
<pre style="padding: 0.88889em;max-width: 100%;overflow-wrap: normal;font-size: 0.9em;word-break: normal;overflow: auto;background: rgb(246, 246, 246);border-radius: 4px;box-sizing: border-box !important;">'if' ' ' '(' '1' ' ' '>' ' ' ')' ' ' '{'<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />'\n ' 'alert' '(' '"if \"1 > 0\""' ')' ';' '\n' '}'<br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
</pre>
注意其中"if "1 > 0""是作为一个语法单元存在,没有再查分成if、1、>、0这样,而且其中的转译符会阻止字符串早结束。
这拆分过程其实没啥可取巧的,就是简单粗暴地一个字符一个字符地遍历,然后分情况讨论,整个实现方法就是顺序遍历和大量的条件判断。我用一个简单的实现来解释,在关键的地方注释,我们只考虑上面那段代码里存在的语法单元类型。
<p><pre style="padding: 0.88889em;max-width: 100%;overflow-wrap: normal;font-size: 0.9em;word-break: normal;overflow: auto;background: rgb(246, 246, 246);border-radius: 4px;box-sizing: border-box !important;">function tokenizeCode (code) {
const tokens = []; // 结果数组
for (let i = 0; i
发表评论
热门文章
Spimes主题专为博客、自媒体、资讯类的网站设计....
一款个人简历主题,可以简单搭建一下,具体也比较简单....
仿制主题,Typecho博客主题,昼夜双版设计,可....
用于作品展示、资源下载,行业垂直性网站、个人博客,....
尘集杂货铺和官网1t5-cn
11月11日
[已回复]
希望主题和播放器能支持SQLite数据库,AI能多个讯飞星火