抽象语法树(Abstract Syntax Tree
)简称 AST
,是源代码的抽象语法结构的树状表现形式。webpack
、eslint
等不少工具库的核心都是经过抽象语法书这个概念来实现对代码的检查、分析等操做。今天我为你们分享一下 JavaScript 这类解释型语言的抽象语法树的概念javascript
咱们经常使用的浏览器就是经过将 js 代码转化为抽象语法树来进行下一步的分析等其余操做。因此将 js 转化为抽象语法树更利于程序的分析。html
如上图中变量声明语句,转换为 AST 以后就是右图中显示的样式vue
左图中对应的:java
var
是一个关键字AST
是一个定义者=
是 Equal 等号的叫法有不少形式,在后面咱们还会看到is tree
是一个字符串;
就是 Semicolon首先一段代码转换成的抽象语法树是一个对象,该对象会有一个顶级的 type 属性 Program
;第二个属性是 body
是一个数组。node
body
数组中存放的每一项都是一个对象,里面包含了全部的对于该语句的描述信息webpack
type: 描述该语句的类型 --> 变量声明的语句 kind: 变量声明的关键字 --> var declaration: 声明内容的数组,里面每一项也是一个对象 type: 描述该语句的类型 id: 描述变量名称的对象 type: 定义 name: 变量的名字 init: 初始化变量值的对象 type: 类型 value: 值 "is tree" 不带引号 row: "\"is tree"\" 带引号
JavaScript
是解释型语言,通常经过 词法分析 -> 语法分析 -> 语法树,就能够开始解释执行了git
词法分析:也叫扫描
,是将字符流转换为记号流(tokens
),它会读取咱们的代码而后按照必定的规则合成一个个的标识github
好比说:var a = 2
,这段代码一般会被分解成 var、a、=、2
web
[ { type: 'Keyword', value: 'var' }, { type: 'Identifier', value: 'a' }, { type: 'Punctuator', value: '=' }, { type: 'Numeric', value: '2' }, ];
当词法分析源代码的时候,它会一个一个字符的读取代码,因此很形象地称之为扫描 - scans
。当它遇到空格、操做符,或者特殊符号的时候,它会认为一个话已经完成了。express
语法分析:也称解析器
,将词法分析出来的数组转换成树的形式,同时验证语法。语法若是有错的话,抛出语法错误。
{ ... "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "a" }, ... }
语法分析成 AST ,咱们能够在这里在线看到效果 http://esprima.org
好比说,有个函数 function a() {}
我想把它变成 function b() {}
好比说,在 webpack
中代码编译完成后 require('a') --> __webapck__require__("*/**/a.js")
下面来介绍一套工具,能够把代码转成语法树而后改变节点以及从新生成代码
准备工具:
在推荐一个经常使用的 AST 在线转换网站:https://astexplorer.net/
好比说一段代码 function getUser() {}
,咱们把函数名字更改成 hello
,看代码流程
看如下代码,简单说明 AST
遍历流程
const esprima = require('esprima'); const estraverse = require('estraverse'); const code = `function getUser() {}`; // 生成 AST const ast = esprima.parseScript(code); // 转换 AST,只会遍历 type 属性 // traverse 方法中有进入和离开两个钩子函数 estraverse.traverse(ast, { enter(node) { console.log('enter -> node.type', node.type); }, leave(node) { console.log('leave -> node.type', node.type); }, });
输出结果以下:
由此能够获得 AST 遍历的流程是深度优先,遍历过程以下:
此时咱们发现函数的名字在 type
为 Identifier
的时候就是该函数的名字,咱们就能够直接修改它即可实现一个更改函数名字的 AST
工具
// 转换树 estraverse.traverse(ast, { // 进入离开修改都是能够的 enter(node) { console.log('enter -> node.type', node.type); if (node.type === 'Identifier') { node.name = 'hello'; } }, leave(node) { console.log('leave -> node.type', node.type); }, }); // 生成新的代码 const result = escodegen.generate(ast); console.log(result); // function hello() {}
提到 AST 咱们确定会想到 babel,自从 Es6 开始大规模使用以来,babel 就出现了,它主要解决了就是一些浏览器不兼容 Es6 新特性的问题,其实就把 Es6 代码转换为 Es5 的代码,兼容全部浏览器,babel 转换代码其实就是用了 AST,babel 与 AST 就有着很一种特别的关系。
那么咱们就在 babel 的中来使用 AST,看看 babel 是如何编译代码的(不讲源码啊)
须要用到两个工具包 @babel/core
、@babel/preset-env
当咱们配置 babel 的时候,不论是在 .babelrc
或者 babel.config.js
文件里面配置的都有 presets
和 plugins
两个配置项(还有其余配置项,这里不作介绍)
// .babelrc { "presets": ["@babel/preset-env"], "plugins": [] }
当咱们配置了 presets
中有 @babel/preset-env
,那么 @babel/core
就会去找 preset-env
预设的插件包,它是一套
babel 核心包并不会去转换代码,核心包只提供一些核心 API,真正的代码转换工做由插件或者预设来完成,好比要转换箭头函数,会用到这个 plugin,@babel/plugin-transform-arrow-functions
,当须要转换的要求增长时,咱们不可能去一一配置相应的 plugin,这个时候就能够用到预设了,也就是 presets。presets 是 plugins 的集合,一个 presets 内部包含了不少 plugin。
如今咱们有一个箭头函数,要想把它转成普通函数,咱们就能够直接这么写:
const babel = require('@babel/core'); const code = `const fn = (a, b) => a + b`; // babel 有 transform 方法会帮咱们自动遍历,使用相应的预设或者插件转换相应的代码 const r = babel.transform(code, { presets: ['@babel/preset-env'], }); console.log(r.code); // 打印结果以下 // "use strict"; // var fn = function fn() { return a + b; };
此时咱们能够看到最终代码会被转成普通函数,可是咱们,只须要箭头函数转通函数的功能,不须要用这么大一套包,只须要一个箭头函数转普通函数的包,咱们实际上是能够在 node_modules
下面找到有个叫作 plugin-transform-arrow-functions
的插件,这个插件是专门用来处理 箭头函数的,咱们就能够这么写:
const r = babel.transform(code, { plugins: ['@babel/plugin-transform-arrow-functions'], }); console.log(r.code); // 打印结果以下 // const fn = function () { return a + b; };
咱们能够从打印结果发现此时并无转换咱们变量的声明方式仍是 const 声明,只是转换了箭头函数
此时,咱们就能够本身来写一些插件,来实现代码的转换,中间处理代码的过程就是使用前面提到的 AST 的处理逻辑
如今咱们来个实战把 const fn = (a, b) => a + b
转换为 const fn = function(a, b) { return a + b }
首先咱们在在线分析 AST 的网站上分析 const fn = (a, b) => a + b
和 const fn = function(a, b) { return a + b }
看二者语法树的区别
根据咱们分析可得:
ArrowFunctionExpression
,而是函数表达式了 FunctionExpression
箭头函数表达式(ArrowFunctionExpression)
转换为 函数表达式(FunctionExpression)
二进制表达式(BinaryExpression)
包裹在 返回语句中(ReturnStatement)
而后 push 到 代码块中(BlockStatement)
,在 babel 中,咱们开发 plugins 的时候要用到访问者模式,就是说在访问到某一个路径的时候进行匹配,而后在对这个节点进行修改,好比说上面的当咱们访问到 ArrowFunctionExpression
的时候,对 ArrowFunctionExpression
进行修改,变成普通函数
那么咱们就能够这么写:
const babel = require('@babel/core'); const code = `const fn = (a, b) => a + b`; // 转换后 const fn = function(a, b) { return a + b } const arrowFnPlugin = { // 访问者模式 visitor: { // 当访问到某个路径的时候进行匹配 ArrowFunctionExpression(path) { // 拿到节点 const node = path.node; console.log('ArrowFunctionExpression -> node', node); }, }, }; const r = babel.transform(code, { plugins: [arrowFnPlugin], }); console.log(r);
此时咱们拿到的结果是这样的节点结果是 这样的,其实就是 ArrowFunctionExpression
的 AST,此时咱们要作的是把 ArrowFunctionExpression
的结构替换成 FunctionExpression
的结构,可是须要咱们组装相似的结构,这么直接写很麻烦,可是 babel 为咱们提供了一个工具叫作 @babel/types
@babel/types
有两个做用:
而后咱们使用的时候,须要常常查文档,由于里面的节点类型特别多,不是作编译相关工做的是记不住怎么多节点的
那么接下来咱们就开始生成一个 FunctionExpression
,而后把以前的 ArrowFunctionExpression
替换掉,咱们能够看 types
文档,找到 functionExpression
,该方法接受相应的参数咱们传递过去便可生成一个 FunctionExpression
t.functionExpression(id, params, body, generator, async);
BlockStatement
咱们须要生成一个还须要生成一个 BlockStatement
,咱们接着看文档找到 BlockStatement
接受的参数
t.blockStatement(body, directives);
看文档说明,blockStatement
接受一个 body,那咱们把以前的 body 拿过来就能够直接用,不过这里 body 接受一个数组
咱们在看 AST 结构,函数表达式中的 BlockStatement
中的 body
是一个 ReturnStatement
组成的集合,因此还须要生成一个 ReturnStatement
如今咱们就能够改写 AST 了
ArrowFunctionExpression(path) { // 拿到节点而后替换节点 const node = path.node; // 拿到函数的参数 const params = node.params; const returnStatement = t.returnStatement(node.body); const blockStatement = t.blockStatement([returnStatement]); const functionExpression = t.functionExpression(null, params, blockStatement); // 替换原来的函数 path.replaceWith(functionExpression); }, // 结果 const fn = function (a, b) { return a + b; };
固然若是没有返回语句的话咱们也能够生成一个 ExpressionStatement
,只须要把 returnStatement
改成 ExpressionStatement
其余逻辑不变
ArrowFunctionExpression(path) { // 拿到节点而后替换节点 const node = path.node; // 拿到函数的参数 const params = node.params; // 把 returnStatement 换成 expressionStatement 便可 const expressionStatement = t.expressionStatement(node.body); const blockStatement = t.blockStatement([expressionStatement]); const functionExpression = t.functionExpression(null, params, blockStatement); // 替换原来的函数 path.replaceWith(functionExpression); }, // 结果 const fn = function (a, b) { a + b; };
在开发中,咱们引入 UI 框架,好比 vue 中用到的 element-ui
,vant
或者 React
中的 antd
都支持全局引入和按需引入,默认是全局引入,若是须要按需引入就须要安装一个 babel-plugin-import
的插件,将全局的写法变成按需引入的写法。
就拿我最近开发移动端用的 vant 为例, import { Button } from 'vant'
这种写法通过这个插件以后会变成 import Button from 'vant/lib/Button'
这种写法,引用整个 vant 变成了我只用了 vant 下面的某一个文件,打包后的文件会比所有引入的文件大小要小不少
import { Button, Icon } from 'vant'
写法转换为import Button from 'vant/lib/Button'; import Icon from 'vant/lib/Icon'
看一下两个语法树的区别
根据两张图分析咱们能够获得一些信息:
specifiers
)是两个 ImportSpecifier
,第二张图里面是分开的,并且都是 ImportDefaultSpecifier
source
也不同ImportDeclaration
变成多个 ImportDeclaration
, 而后把单个 import 解构引入的 specifiers
部分 ImportSpecifier
转换成多个 ImportDefaultSpecifier
并修改对应的 source
便可为了方便传递参数,此次咱们写到一个函数里面,能够方便传递转换后拼接的目录
这里咱们须要用到的几个类型,也须要在 types 官网上找对应的解释
importDeclaration
类型/** * @param {Array<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier>} specifiers (required) * @param {StringLiteral} source (required) */ t.importDeclaration(specifiers, source);
importDeclaration
中须要生成 ImportDefaultSpecifier
/** * @param {Identifier} local (required) */ t.importDefaultSpecifier(local);
importDeclaration
中还须要生成一个 StringLiteral
/** * @param {string} value (required) */ t.stringLiteral(value);
按照上面的分析,咱们开始上代码
const babel = require('@babel/core'); const t = require('@babel/types'); const code = `import { Button, Icon } from 'vant'`; // import Button from 'vant/lib/Button' // import Icon from 'vant/lib/Icon' function importPlugin(opt) { const { libraryDir } = opt; return { visitor: { ImportDeclaration(path) { const node = path.node; // console.log("ImportDeclaration -> node", node) // 获得节点的详细说明,而后转换成多个的 import 声明 const specifiers = node.specifiers; // 要处理这个咱们作一些判断,首先判断不是默认导出咱们才处理,要考虑 import vant, { Button, Icon } from 'vant' 写法 // 还要考虑 specifiers 的长度,若是长度不是 1 而且不是默认导出咱们才须要转换 if (!(specifiers.length === 1 && t.isImportDefaultSpecifier(specifiers[0]))) { const result = specifiers.map(specifier => { const local = specifier.local; const source = t.stringLiteral( `${node.source.value}/${libraryDir}/${specifier.local.name}` ); // console.log("ImportDeclaration -> specifier", specifier) return t.importDeclaration([t.importDefaultSpecifier(local)], source); }); console.log('ImportDeclaration -> result', result); // 由于此次要替换的 AST 不是一个,而是多个的,因此须要 `path.replaceWithMultiple(result)` 来替换,可是一执行发现死循环了 path.replaceWithMultiple(result); } }, }, }; } const r = babel.transform(code, { plugins: [importPlugin({ libraryDir: 'lib' })], }); console.log(r.code);
看打印结果和转换结果彷佛没什么问题,这个插件几乎就实现了
可是咱们考虑一种状况,若是用户不所有按需加载了,按需加载只是一种选择,若是用户这么写了 import vant, { Button, Icon } from 'vant'
,那么咱们这个插件就出现问题了
若是遇到这种写法,那么默认导入的他的 source
应该是不变的,咱们要把原来的 source
拿出来
因此还须要判断一下,每个 specifier
是否是一个 ImportDefaultSpecifier
而后处理不一样的 source
,完整处理逻辑应该以下
function importPlugin(opt) { const { libraryDir } = opt; return { visitor: { ImportDeclaration(path) { const node = path.node; // console.log("ImportDeclaration -> node", node) // 获得节点的详细说明,而后转换成多个的 import 声明 const specifiers = node.specifiers; // 要处理这个咱们作一些判断,首先判断不是默认导出咱们才处理,要考虑 import vant, { Button, Icon } from 'vant' 写法 // 还要考虑 specifiers 的长度,若是长度不是 1 而且不是默认导出咱们才须要转换 if (!(specifiers.length === 1 && t.isImportDefaultSpecifier(specifiers[0]))) { const result = specifiers.map(specifier => { let local = specifier.local, source; // 判断是否存在默认导出的状况 if (t.isImportDefaultSpecifier(specifier)) { source = t.stringLiteral(node.source.value); } else { source = t.stringLiteral( `${node.source.value}/${libraryDir}/${specifier.local.name}` ); } return t.importDeclaration([t.importDefaultSpecifier(local)], source); }); path.replaceWithMultiple(result); } }, }, }; }
在 babel 官网上有一句话 Babylon is a JavaScript parser used in Babel.
babel
使用的引擎是 babylon
,Babylon
并不是 babel
团队本身开发的,而是 fork 的 acorn
项目,acorn
的项目本人在很早以前在兴趣部落 1.0 在构建中使用,为了是作一些代码的转换,是很不错的一款引擎,不过 acorn
引擎只提供基本的解析 ast
的能力,遍历还须要配套的 acorn-travesal
, 替换节点须要使用 acorn-,而这些开发,在 Babel 的插件体系开发下,变得一体化了(摘自 AlloyTeam 团队的剖析 babel)
使用 babylon 编写一个数组 rest 转 Es5 语法的插件
把 const arr = [ ...arr1, ...arr2 ]
转成 var arr = [].concat(arr1, arr2)
咱们使用 babylon 的话就不须要使用 @babel/core
了,只须要用到他里面的 traverse
和 generator
,用到的包有 babylon、@babel/traverse、@babel/generator、@babel/types
先来看一下两棵语法树的区别
根据上图咱们分析得出:
这段代码的核心生成一个 callExpression 调用表达式,因此对应官网上的类型,咱们分析须要用到的 api
/** * @param {Expression} callee (required) * @param {Array<Expression | SpreadElement | JSXNamespacedName>} source (required) */ t.callExpression(callee, arguments);
/** * @param {Expression} object (required) * @param {if computed then Expression else Identifier} property (required) * @param {boolean} computed (default: false) * @param {boolean} optional (default: null) */ t.memberExpression(object, property, computed, optional);
/** * @param {Array<null | Expression | SpreadElement>} elements (default: []) */ t.arrayExpression(elements);
/** * @param {LVal} id (required) * @param {Expression} init (default: null) */ t.variableDeclarator(id, init); /** * @param {"var" | "let" | "const"} kind (required) * @param {Array<VariableDeclarator>} declarations (required) */ t.variableDeclaration(kind, declarations);
const babylon = require('babylon'); // 使用 babel 提供的包,traverse 和 generator 都是被暴露在 default 对象上的 const traverse = require('@babel/traverse').default; const generator = require('@babel/generator').default; const t = require('@babel/types'); const code = `const arr = [ ...arr1, ...arr2 ]`; // var arr = [].concat(arr1, arr2) const ast = babylon.parse(code, { sourceType: 'module', }); // 转换树 traverse(ast, { VariableDeclaration(path) { const node = path.node; const declarations = node.declarations; console.log('VariableDeclarator -> declarations', declarations); const kind = 'var'; // 边界断定 if ( node.kind !== kind && declarations.length === 1 && t.isArrayExpression(declarations[0].init) ) { // 取得以前的 elements const args = declarations[0].init.elements.map(item => item.argument); const callee = t.memberExpression(t.arrayExpression(), t.identifier('concat'), false); const init = t.callExpression(callee, args); const declaration = t.variableDeclarator(declarations[0].id, init); const variableDeclaration = t.variableDeclaration(kind, [declaration]); path.replaceWith(variableDeclaration); } }, });
异步终极解决方案:async + await
以同步的写法处理异步代码。一切都好,惟一有问题的就是要想捕获代码出现的问题须要使用 try/catch
包裹 await 代码片断。为了程序的健壮性,就可能须要在 async 中频繁的书写 try/catch
逻辑,此时咱们能够就可使用 ast 捕获到相应的代码而后处理没有被 try/catch
的 await
语句
// 转换前 async function func() { await asyncFn(); }
// 转换后 async function func() { try { await asyncFn(); } catch (e) {} }
咱们发现咱们要作的就是在 AwaitExpression
await 表达式外层包裹一层 TryStatement
try 语句
那咱们要作的就是生成一个 tryStatement,查看对应的 api
/** * @param {BlockStatement} block (required) * @param {CatchClause} handler (default: null) * @param {BlockStatement} finalizer (default: null) */ t.tryStatement(block, handler, finalizer);
暂时先不考虑 CatchClause,先生成 try
/** * @param {Array<Statement>} body (required) * @param {Array<Directive>} directives (default: []) */ t.blockStatement(body, directives);
再根据 ast 树结构中获得,body 是由表达式语句(ExpressionStatement)组成
/** * @param {Expression} expression (required) */ t.expressionStatement(expression);
在 expressionStatement 中须要的 expression 就是咱们的当前捕获到的节点,那么咱们就能够开始写代码了
咱们要在 AwaitExpression 中捕获代码,还须要判断该代码段的父节点没有被 try/catch 包裹,能够利用 path 参数的 findParent 方法向上遍历全部父节点,判断是否被 try/catch 的 Node 包裹
AwaitExpression(path) { // 首先保证 await 语句没有被 try/catch 包裹 if (path.findParent(path => t.isTryStatement(path.node))) return; const expression = t.expressionStatement(path.node); const tryBlock = t.blockStatement([expression]); // 生成 catch --> console.log(e) const paramsE = t.identifier('e'); const memberExpression = t.MemberExpression(t.identifier('console'), t.identifier('log')); const consoleExpression = t.expressionStatement(t.callExpression(memberExpression, [paramsE])); const catchClause = t.catchClause(paramsE, t.blockStatement([consoleExpression])); const tryStatement = t.tryStatement(tryBlock, catchClause); // 数组 path.replaceWithMultiple([tryStatement]); } // 获得的结果: // async function func() { // try { // await asyncFn(); // } catch (e) { // console.log(e); // } // }
另外咱们要考虑到 await 表达式可能出现其余状况,能够直接声明变量赋值,能够直接赋值,而后就是刚刚处理的直接一个表达式
// 声明变量赋值 const r = await asyncFn(); // 赋值 r = await asyncFn(); // 就是一个表达式 await asyncFn();
此时咱们能够区分不一样的状况作不一样的处理,再次观察语法树,发现他们的区别在 blockStatement 节点下面,那么咱们就能够直接替换这一级就能够,顺便把 catch 语句补充完整
此时咱们输入的代码以下:
async function func() { const r = await asyncFn1(); res = await asyncFn2(); await asyncFn3(); }
处理过程:
AwaitExpression(path) { // 首先保证 await 语句没有被 try/catch 包裹 if (path.findParent(path => t.isTryStatement(path.node))) return; const parent = path.parent; let replacePath = null; if (t.isVariableDeclarator(parent) || t.isAssignmentExpression(parent)) { // 赋值和声明的方式结构相似,都是在 AwaitExpression 中 path 的 parentPath.parentPath 上的节点就是 blockStatement 所须要的的参数,能够直接这么替换 replacePath = path.parentPath.parentPath; } else { // 若是只是表达式的话,path.parentPath.node 就是 blockStatement 参数 replacePath = path.parentPath; } const tryBlock = t.blockStatement([replacePath.node]); // 生成 catch --> new Error(e) const paramsE = t.identifier('e'); const throwStatement = t.throwStatement(t.newExpression(t.identifier('Error'), [paramsE])); const catchClause = t.catchClause(paramsE, t.blockStatement([throwStatement])); const tryStatement = t.tryStatement(tryBlock, catchClause); replacePath.replaceWithMultiple([tryStatement]); }, // 获得结果 // async function func() { // try { // const r = await asyncFn1(); // } catch (e) { // throw new Error(e); // } // try { // res = await asyncFn2(); // } catch (e) { // throw new Error(e); // } // try { // await asyncFn3(); // } catch (e) { // throw new Error(e); // } // }
和抽象语法树相对的是具体语法树(Concrete Syntax Tree
)简称 CST
(一般称做分析树)。通常的,在源代码的翻译和编译过程当中,语法分析器建立出分析树。一旦 AST 被建立出来,在后续的处理过程当中,好比语义分析阶段,会添加一些信息。可参考抽象语法树和具体语法树有什么区别?
关于 node 类型,全集大体以下:
(parameter) node: Identifier | SimpleLiteral | RegExpLiteral | Program | FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | SwitchCase | CatchClause | VariableDeclarator | ExpressionStatement | BlockStatement | EmptyStatement | DebuggerStatement | WithStatement | ReturnStatement | LabeledStatement | BreakStatement | ContinueStatement | IfStatement | SwitchStatement | ThrowStatement | TryStatement | WhileStatement | DoWhileStatement | ForStatement | ForInStatement | ForOfStatement | VariableDeclaration | ClassDeclaration | ThisExpression | ArrayExpression | ObjectExpression | YieldExpression | UnaryExpression | UpdateExpression | BinaryExpression | AssignmentExpression | LogicalExpression | MemberExpression | ConditionalExpression | SimpleCallExpression | NewExpression | SequenceExpression | TemplateLiteral | TaggedTemplateExpression | ClassExpression | MetaProperty | AwaitExpression | Property | AssignmentProperty | Super | TemplateElement | SpreadElement | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern | ClassBody | MethodDefinition | ImportDeclaration | ExportNamedDeclaration | ExportDefaultDeclaration | ExportAllDeclaration | ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier
Babel 有文档对 AST 树的详细定义,可参考这里
代码以存放到 GitHub,地址