乐趣区

关于ast:走进AST

前言:AST 曾经深刻的存在咱们我的项目脚手架中,然而咱们缺不理解他,本文率领大家一起体验 AST,感受一下解决问题另一种办法

什么是 AST

在讲之前先简略介绍一下什么 AST, 形象语法树(Abstract Syntax Tree)简称 AST,是源代码的形象语法结构的树状表现形式。
平时很多库都有他的影子:

例如 babel, es-lint, node-sass, webpack 等等。

OK 让咱们看下代码转换成 AST 是什么样子。

const ast = 'tree'

这是一行简略的申明代码,咱们看下他转换成 AST 的样子

咱们发现整个树的根节点是 Program,他有一个子节点 bodybody 是一个数组,数组中还有一个子节点 VariableDeclarationVariableDeclaration中示意 const ast = 'tree' 这行代码的申明,具体的解析如下:

type: 形容语句的类型,此处是一个变量申明类型
kind: 形容申明类型,相似的值有 'var' 'let'
declarations: 申明内容的数组,其中每一项都是一个对象
------------type: 形容语句的类型,此处是一个变量申明类型
------------id: 被申明字段的形容
----------------type: 形容语句的类型,这里是一个标识符
----------------name: 变量的名字
------------init: 变量初始化值的形容
----------------type: 形容语句的类型,这里是一个标识符
----------------name: 变量的值

大体上的构造是这样,body 下的每个节点还有一些字段没有给大家阐明,例如:地位信息,以及一些没有值的 key 都做了暗藏,举荐大家能够去 asteplorer 这个网站去试试看。

总结一下,AST 就是把代码通过编译器变成树形的表达形式。

如何生成 AST

如何生成把纯文本的代码变成 AST 呢?编辑器生成语法树个别分为三个步骤

  • 词法剖析
  • 语法分析
  • 生成语法树
  1. 词法剖析:也叫做 扫描。它读取咱们的代码,而后把它们依照预约的规定合并成一个个的标识 tokens。同时,它会移除空白符,正文,等。最初,整个代码将被宰割进一个 tokens 列表(或者说一维数组)。

比方说下面的例子 const ast = 'tree',会被剖析为const、ast、=、'tree'

const ast = 'tree';
[{ type: 'keyword', value: 'const'},  
 {type: 'identifier', value: 'a'},  
 {type: 'punctuator', value: '='},  
 {type: 'numeric', value: '2'},  
]

当词法剖析源代码的时候,它会一个一个字母地读取代码,所以很形象地称之为扫描 -scans;当它遇到空格,操作符,或者特殊符号的时候,它会认为一个话曾经实现了。

2. 语法分析:也称为解析器。它会将词法剖析进去的数组转化成树形的表达形式。同时,验证语法,语法如果有错的话,抛出语法错误。

3. 生成树:当生成树的时候,解析器会删除一些没必要的标识 tokens(比方不残缺的括号),因而 AST 不是 100% 与源码匹配的,然而曾经能让咱们晓得如何解决了。说个题外话,解析器 100% 笼罩所有代码构造生成树叫做 CST(具体语法树)

是否通过第三方库来生成?

有很多的第三方库能够用来实战操作,能够去 asteplorer 这个网站去找你喜爱的第三方库,这里不限于 javascript,其余的语言也能够在这个网站上找到。
如图:

对于javascript 的第三方库,这里给大家举荐 babel 的外围库babylon

// yarn add babylon
import * as babylon from 'babylon';

const code = `
    const ast = 'tree'
`

const ast = babylon.parse(code); // ast

如何实际

ok,当初咱们曾经晓得如何把咱们的代码变成 AST 了,然而事实中,咱们常常会应用到代码的转换,比方说 jsx -> js, es6 -> es5, 是的就是 babel,咱们来看看 babel 是如何转换代码的。

大体上 babel 转换代码分为三步

1. 通过 `babylon` 生成 `AST`
2. 遍历 `AST` 同时通过指定的拜访器拜访须要批改的节点
3. 生成代码

看一个简略的例子一起了解一下
生成AST

import * as babylon from 'babylon';
// 这里也能够应用 import parser from '@babel/parser'; 这个来生成语法树
const code = `
    const ast = 'tree'
    console.log(ast);
`

const ast = babylon.parse(code); // ast

遍历 AST 同时通过拜访器 CallExpression 来拜访 console.log(ast) 并删除它

import traverse from '@babel/traverse'
import t from '@babel/types';
// 2 遍历

const visitor = {CallExpression(path) {const { callee} = path.node;
        if (t.isMemberExpression(callee) &&
            callee.object.name === 'console' &&
            callee.property.name === 'log'
        ) {path.remove();
        }
    },
}

traverse.default(ast, visitor);

生成新代码

import generator from '@babel/generator';
generator.default(ast);

简略的答疑:CallExpression 示意这是一个调用,为什么还要做更深刻的判断呢,因为间接的函数调用 foo() 这也是一个 CallExpression,A.foo()这也是一个 CallExpression, 所以要更深刻的判断

好的,代码转换实现!值得庆贺。咱们能够看到 第一步生成 AST第三步生成新代码 都由 babel 替咱们做了,咱们真正操作的中央在于第二步:通过拜访器操作须要操作的节点。

由此可见咱们开发 babel-plugin 的时候,也只须要关注 visitor 这部分就好。

上述代码改为 babel-plugin 示例:

module.export = function plugin({types: t}) {
    return {
        visitor: {CallExpression(path) {const { node} = path;
                if (t.isMemberExpression(node.callee) &&
                node.callee.object.name === 'console' &&
                node.callee.property.name === 'log'
                ) {path.remove();
                }
            },

        },
    };
}

将这个插件退出到你的 babel 插件列表中,能够看到它真的失效了,一切都是这么简略。so amazing!

结语

结尾提到的罕用库 prettire, eslint, css-loader 等等其实都是学生成AST, 而后再操作AST,最初在生成代码。只不过操作AST 的过程很简单,触类旁通在我的项目里,组件库降级,组件批量替换都能够应用这个思路。甚至能够依据业务做一些本人业务方的 babel-plugin 都行。
感谢您的浏览,有问题能够在评论区交换~

帮忙链接
如何开发一个 babel-plugin
《AST for JavaScript developers》

退出移动版