抽象语法树
自定义插件第一小节我们来看一下抽象语法树。
官方对 Postcss 的原理介绍如下:
PostCSS takes a CSS file and provides an API to analyze and modify its rules (by transforming them into an Abstract Syntax Tree). This API can then be used by plugins to do a lot of useful things, e.g., to find errors automatically, or to insert vendor prefixes.
Postcss 的工作流程如下图所示:
关于抽象语法树这个概念其实是非常重要的,你在很多地方都能看到它。
当我们遇到一个难以理解的术语的时候,有一个最简单的方式就是“拆词”。“抽象语法树”经过拆词就可以拆解为三个词:
- 抽象
- 语法
- 树
我们首先来看树。“树”实际上是一种数据结构。
所谓数据结构,就是指数据在计算机中组织和存储的一种方式。数据结构通常会分为两类:
- 线性数据结构
- 数组(Array):一种连续存储空间中的固定大小的数据项集合。数组将相同类型的元素存储在连续的内存位置中,允许通过索引快速访问元素。
- 链表(Linked List):一种由节点组成的线性集合,每个节点包含数据和指向下一个节点的指针。链表允许在不重新分配整个数据结构的情况下插入和删除元素。
- 栈(Stack):一种遵循后进先出(LIFO,Last In First Out)原则的线性数据结构。在栈中,数据项的添加和移除都在同一端进行,称为栈顶。
- 队列(Queue):一种遵循先进先出(FIFO,First In First Out)原则的线性数据结构。在队列中,数据项的添加在一端进行(队尾),移除在另一端进行(队头)。
- 非线性数据结构
- 树(Tree):一种分层结构,由节点组成,其中有一个特殊的节点称为根节点,其余节点按照层级组织。每个节点(除根节点外)都有一个父节点,可以有多个子节点。常见的树结构有二叉树、红黑树、AVL 树等。
- 图(Graph):一种由顶点(节点)和边组成的数据结构,边连接了顶点。图可以是有向的(边有方向)或无向的(边无方向)。图可用于表示具有复杂关系的数据集合。
接下来我们聚焦到“树”这种数据结构,树这种非线性的数据结构,在解决某些问题的时候有一些显著的特点:
- 层次关系:通过树结构能够非常自然的表示出数据之间的层次关系,这是其他数据结构办不到的。
- 搜索效率:通过树的结构(平衡二叉树),在执行搜索、插入以及删除等操作时,效率是比较高的,时间复杂度通常为 O(log n),n是树的节点数量。一般比线性的数据结构(数组、链表)要高很多。
- 动态数据集合:与数组等固定大小的数据结构相比,树结构可以方便地添加、删除和重新组织节点。这使得树结构非常适合用于动态变化的数据集合。
- 有序存储:在二叉搜索树等有序树结构中,数据按照一定的顺序进行组织。这允许我们在 O(log n) 时间内完成有序数据集合的操作,如查找最大值、最小值和前驱、后继等。
- 空间优化:在某些应用场景中,树结构可以有效地节省空间。例如,字典树(Trie)可以用于存储大量字符串,同时节省空间,因为公共前缀只存储一次。
- 分治策略:树结构天然地适应分治策略,可以将复杂问题分解为较小的子问题并递归求解。许多高效的算法都基于树结构,如排序算法(归并排序、快速排序)、图算法(最小生成树、最短路径等)。
上面的这些优点,如果你没有系统的学习过数据结构相关的知识,你是没有办法很多的进行理解。但是这个并不影响我们学习抽象语法树。上面所罗列的这些特点只是为了说明一点:树这种数据结构是存在很多优点的,所以我们能够在很多地方看到树的身影,例如:DOM树、CSSOM树、语法树。
接下来我们重点来说语法树。什么是语法树?简单来讲,就是将我们所书写的源代码转为树的结构。
var a = 42;
var b = 5;
function addA(d) {
return a + d;
}
var c = addA(2) + b;
对于编译器或者解释器来讲,上面的代码它们并不能够理解。上面的这些代码对于编译器或者解释器来讲,无非就是一段字符串而已:
'var a = 42;var b = 5;function addA(d) {return a + d;}var c = addA(2) + b;'
因此要执行这个代码,编译器或解释器首先第一步就是要分析出来这个字符串里面哪些是关键字,哪些是标志符,哪些是运算符。之后会形成一个一个的 token,例如上面的代码,最终就会形成各种各样的 token(不可再拆分、最小的单位):
Keyword(var) Identifier(a) Punctuator(=) Numeric(42) Punctuator(;) Keyword(var)
Identifier(b) Punctuator(=) Numeric(5) Punctuator(;) Keyword(function)
Identifier(addA) Punctuator(() Identifier(d) Punctuator()) Punctuator({)
Keyword(return) Identifier(a) Punctuator(+) Identifier(d) Punctuator(;)
Punctuator(}) Keyword(var) Identifier(c) Punctuator(=) Identifier(addA)
Punctuator(() Numeric(2) Punctuator()) Punctuator(+) Identifier(b) Punctuator(;)
拆解成一个一个的 token 之后,会将这些 token 以树的形式来存储,最终会形成如下的一个树结构:
在 www.jointjs.com/demos/abstr… 这个网站可以看到 JS 代码所形成的抽象语法树长什么样子。
至此,你就知道什么叫做语法树。
最后我们还需要解释一下什么叫做“抽象语法树”。
抽象(abstraction)是一种思维方式。所谓抽象,指的是从具体的事物里面提取出本质特征、规律,忽略不相关、不重要的细节。在计算机科学和编程里面,抽象是一种非常重要的方法,因为抽象能够将一个复杂的问题抽离成简单的、更加容易理解的问题
抽象语法树是将源代码抽象成一种更高阶别的表示方式,只关注代码的结构和语法,会去忽略空格、换行、制表符之类的表达细节。
最后说一下抽象语法树的优点:
-
易于操作和遍历:可以更方便地进行操作和遍历。AST 中的每个节点都有确定的类型和结构,这使得插件作者可以轻松地定位和修改特定类型的节点,而无需解析和操作原始 CSS 文本。
-
易于扩展:使用 AST 可以轻松地支持新的 CSS 语法和特性。只需在 AST 中添加相应的节点类型和规则,就可以在插件中处理新的语法结构,而无需对整个解析器进行重大改动。
-
提高性能:将 CSS 代码转换为 AST 后,可以对整个树进行一次遍历,同时应用多个插件的变换操作。这样可以减少重复解析和操作 CSS 文本的开销,从而提高处理性能。
-
代码重用和模块化:由于 AST 的结构化特性,插件开发者可以在多个插件之间重用和共享操作 AST 的代码。这有助于降低插件间的冗余,并提高代码的模块化程度。
-
易于调试和错误处理:AST 中的每个节点都包含有关其源代码位置的元信息。这使得插件可以在出现错误时提供更具体的错误信息和上下文,从而帮助开发者快速定位和解决问题。
著名的 babel 项目在处理 JS 的时候就是会先将 JS 转为抽象语法树,然后再交给其他的插件做处理。ESlint 工具检查代码是否规范,那它怎么检查的?它其实也是先将代码转为抽象语法树,然后再去检查。我们这里所学习的 postcss 也是同样的原理,只不过它是将 css 代码转为对应的 css 抽象语法树。