阅读 274

前端开发应该了解的抽象语法树和Bable原理

现代前端构建在 AST 之上,无论是 ESlint、Babel、Webpack,还是 css 处理器、代码压缩,都是站立在 AST 的肩膀上。在深入学习 Babel 之前,先了解一些关于 AST 的知识。

上周我还发了一篇 # 学习写一个简单的babel插件,这两篇文章一起看,效果会更好哟。

AST 是什么?

在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于 if-condition-then 这样的条件跳转语句,可以使用带有两个分支的节点来表示。

如果看完定义如果还是一脸懵逼,那么就上一个简单的 ???? ,看看把一个简单函数转成 AST 后的样子。示例中的 AST 可以通过网站 astexplorer在线生成。

// 源码 function sum(a, b) {   return a + b; } 复制代码

// 生成的 AST {   "type": "Program",   "start": 0,   "end": 37,   "body": [     {       "type": "FunctionDeclaration",       "start": 0,       "end": 37,       "id": {         "type": "Identifier",         "start": 9,         "end": 12,         "name": "sum"       },       "expression": false,       "generator": false,       "async": false,       "params": [         {           "type": "Identifier",           "start": 13,           "end": 14,           "name": "a"         },         {           "type": "Identifier",           "start": 16,           "end": 17,           "name": "b"         }       ],       "body": {         "type": "BlockStatement",         "start": 19,         "end": 37,         "body": [           {             "type": "ReturnStatement",             "start": 22,             "end": 35,             "argument": {               "type": "BinaryExpression",               "start": 29,               "end": 34,               "left": {                 "type": "Identifier",                 "start": 29,                 "end": 30,                 "name": "a"               },               "operator": "+",               "right": {                 "type": "Identifier",                 "start": 33,                 "end": 34,                 "name": "b"               }             }           }         ]       }     }   ],   "sourceType": "module" } 复制代码

可以看到 AST 每一层都拥有相似的结构:

{   "type": "FunctionDeclaration",   "id": {},   "params": [],   "body": [] } 复制代码

{   "type": "Identifier",   "name": "" } 复制代码

{   "type": "BinaryExpression",   "left": "",   "operator": "",   "right": "" } 复制代码

这样的每一层结构也被叫做 节点(Node)。 一个 AST 可以由单一的节点或是成百上千个节点构成。 它们组合在一起可以描述用于静态分析的程序语法。

每一个节点都有如下所示的接口:

interface Node {   type: string; } 复制代码

type 字段表示节点的类型(如:FunctionDeclarationIdentifierBinaryExpression)。每一种类型的节点还定义了一些附加属性来进一步描述该节点。

还有一些属性用来描述该节点在原始代码中的位置。

{   "type": "",   "start": 0,   "end": 55,   "loc": {     "start": {       "line": 1,       "column": 0     },     "end": {       "line": 3,       "column": 1     }   } } 复制代码

生成 AST 的步骤

生成 AST 分为两个阶段,分别是词法分析(Lexical Analysis)和语法分析(Syntactic Analysis)。

词法分析/扫描(Scanning)

词法分析阶段从左向右逐行扫描源程序的字符,识别出各个单词,确定单词的类型,将识别出的单词转换成统一的词法单元(token)形式。

token <种别码,属性值>

单词类型种别种别码
关键字if、else、then、...一词一码
标识符变量名、方法名、...多词一码
常量数字、字符串、布尔值一型一码
运算法算术(+ - * / ++ --)
关系(> < == != >= <=)
逻辑(& | ~)
一词一码

一型一码
界限符; () = {}一词一码

举个例子: a + b,这段程序通常会被分解成为下面这些词法单元: a+b,空格是否被当成此法单元,取决于空格在这门语言中的意义。

下面的代码就是利用词法分析网站解析 a + b 后得到的词法单元序列(toekns)。

[   { type: 'Identifier', value: 'a' },   { type: 'Punctuator', value: '+' },   { type: 'Identifier', value: 'b' }, ]; 复制代码

对于词法分析感兴趣的同学可以阅读 @babel/parser 中的词法分析方法 Tokenizer。

语法分析(Parsing)

语法分析器从词法分析器输出的 token 序列中识别出各类短语,并转换成 AST 的形式。

Babel 工作流程

babel工作流

Babel 工作流程分为三步:ParseTransformGenerator

Parse 解析

第一步,Babel 会利用 @babel/parse 包提供的方法,经过 词法分析语法分析 两个步骤,将源代码解析为抽象语法树(AST)的形式。

Transform 转换

第二步,在得到源代码的 AST 后,Babel 会使用 @babel/traverse 遍历整个 AST,并在此过程中根据需求修改 AST。

Visitors(访问者)

Transform 阶段,Babel 会维护一个 visitor 对象,这里对象里定义了用于在 AST 中获取具体节点的方法。下面请看一个例子:

const visitor = {   Identifier(path) {     // 输出当前标识符的名字     console.log(path.node.name);   },   FunctionDeclaration(path) {     // 输出函数定义的名字     console.log(path.node.name);   }, }; 复制代码

注意:Identifier() { ... }Identifier: { enter() { ... } } 的简写形式。

上面的 visitor 对象中定义了 2 个方法,分别是 IdentifierFunctionDeclarationIdentifier 方法在遍历到 type: Identifier 的节点时会执行,而 FunctionDeclaration 方法会在节点的 typeFunctionDeclaration 时执行。

所以在下面的代码中 Identifier() 会被调用四次。

function square(n) {   return n * n; } // Identifier() 会打印出以下内容 // square // n // n // n 复制代码

这些调用都发生在进入节点时,不过我们也可以在退出时调用访问者方法。将访问者方法修改一下:

const visitor = {   Identifier: {     enter(path) {       // 进入时     },     exit(path) {       // 退出时     },   }, }; 复制代码

Paths(路径)

访问者方法接收一个 path 对象参数,这个对象代表当前节点的路径。可以通过 path 访问当前节点、当前节点的父节点、还有其他 Babel 添加到该路径上的一些元数据。

// path 对象 {   "parent": {     "type": "FunctionDeclaration",     "id": {...},     ....   },   "node": {     "type": "Identifier",     "name": "square"   },   ... } 复制代码

State(状态)

修改 AST 时还要考虑代码的状态。比如有以下的例子,我们要将 square 方法中的标识符 n 修改为 x,这时候不能直接使用 Identifier 访问者方法,否则会将方法外的其他同名的标识符一起修改掉。

function square(n) {   return n * n; } const n = 1; 复制代码

对于这种情况,可以使用 FunctionDeclaration 访问者方法,通过这个方法找到 square ,然后再深入递归查找递归查找。

const updateParamNameVisitor = {   Identifier(path) {     if (path.node.name === this.paramName) {       path.node.name = 'x';     }   }, }; const visitor = {   FunctionDeclaration(path) {     const param = path.node.params[0];     const paramName = param.name;     param.name = 'x';     path.traverse({       Identifier(path) {         if (path.node.name === paramName) {           path.node.name = 'x';         }       },     });   }, }; 复制代码

Scopes(作用域)

JavaScript 支持词法作用域,在嵌套的代码块中可以创建出新的作用域。内部作用域可以访问外层作用域的变量、函数、类等,可以统一称这些为引用,而且内部作用域还可以创建和外层作用域同名的引用。因此,在修改 AST 时,必须注意引用的作用域。

幸运的是,Babel 替我们维护了引用和作用域之间的关系,这种关系称之为:绑定(binding)。可以通过 path.scope.bindings 获取当前路径所属作用域内的所有引用的绑定关系。单个绑定看起来就像这样:

{   identifier: node,   scope: scope,   path: path,   kind: 'var',   // 是否被引用   referenced: true,   // 引用数量   references: 3,   // 引用路径   referencePaths: [path, path, path],   constant: false,   constantViolations: [path] } 复制代码

有了这些信息你就可以查找一个绑定的所有引用,并且知道这是什么类型的绑定(参数,定义等等),查找它所属的作用域,或者拷贝它的标识符。 你甚至可以知道它是不是常量,如果不是,那么是哪个路径修改了它。

// 源代码 function log() {   let str = '11111';   str = '22222';   return str; } // 可以利用标识符的引用关系,这样转换代码 function log() {   return '22222'; } 复制代码

Generator 生成

经过上一步的 AST 修改后,需要将其再转换为代码,这时候就会用到 @babel/generator


作者:MaxMeng
链接:https://juejin.cn/post/7033211552161333261

文章分类
前端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐