关于javascript:一文助你搞懂AST

43次阅读

共计 18048 个字符,预计需要花费 46 分钟才能阅读完成。

什么是 AST

形象语法树(Abstract Syntax Tree)简称 AST,是源代码的形象语法结构的树状表现形式。webpackeslint 等很多工具库的外围都是通过形象语法书这个概念来实现对代码的查看、剖析等操作。明天我为大家分享一下 JavaScript 这类解释型语言的形象语法树的概念

咱们罕用的浏览器就是通过将 js 代码转化为形象语法树来进行下一步的剖析等其余操作。所以将 js 转化为形象语法树更利于程序的剖析。

如上图中变量申明语句,转换为 AST 之后就是右图中显示的款式

左图中对应的:

  • var 是一个关键字
  • AST 是一个定义者
  • = 是 Equal 等号的叫法有很多模式,在前面咱们还会看到
  • is tree 是一个字符串
  • ; 就是 Semicolon

首先一段代码转换成的形象语法树是一个对象,该对象会有一个顶级的 type 属性 Program;第二个属性是 body 是一个数组。

body 数组中寄存的每一项都是一个对象,外面蕴含了所有的对于该语句的形容信息

type:         形容该语句的类型  --> 变量申明的语句
kind:         变量申明的关键字  --> var
declaration:  申明内容的数组,外面每一项也是一个对象
            type: 形容该语句的类型
            id:   形容变量名称的对象
                type: 定义
                name: 变量的名字
            init: 初始化变量值的对象
                type:   类型
                value:  值 "is tree" 不带引号
                row:    "\"is tree"\" 带引号

词法剖析和语法分析

JavaScript 是解释型语言,个别通过 词法剖析 -> 语法分析 -> 语法树,就能够开始解释执行了

词法剖析:也叫 扫描,是将字符流转换为记号流(tokens),它会读取咱们的代码而后依照肯定的规定合成一个个的标识

比如说:var a = 2,这段代码通常会被分解成 var、a、=、2

[{ type: 'Keyword', value: 'var'},
  {type: 'Identifier', value: 'a'},
  {type: 'Punctuator', value: '='},
  {type: 'Numeric', value: '2'},
];

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

语法分析:也称 解析器,将词法剖析进去的数组转换成树的模式,同时验证语法。语法如果有错的话,抛出语法错误。

{
  ...
  "type": "VariableDeclarator",
  "id": {
    "type": "Identifier",
    "name": "a"
  },
  ...
}

语法分析成 AST,咱们能够在这里在线看到成果 http://esprima.org

AST 能做什么

  • 语法查看、代码格调查看、格式化代码、语法高亮、谬误提醒、主动补全等
  • 代码混同压缩
  • 优化变更代码,扭转代码构造等

比如说,有个函数 function a() {} 我想把它变成 function b() {}

比如说,在 webpack 中代码编译实现后 require('a') --> __webapck__require__("*/**/a.js")

上面来介绍一套工具,能够把代码转成语法树而后扭转节点以及从新生成代码

AST 解析流程

筹备工具:

  • esprima:code => ast 代码转 ast
  • estraverse: traverse ast 转换树
  • escodegen: ast => code

在举荐一个罕用的 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 遍历的流程是深度优先,遍历过程如下:

批改函数名字

此时咱们发现函数的名字在 typeIdentifier 的时候就是该函数的名字,咱们就能够间接批改它便可实现一个更改函数名字的 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() {}

babel 工作原理

提到 AST 咱们必定会想到 babel,自从 Es6 开始大规模应用以来,babel 就呈现了,它次要解决了就是一些浏览器不兼容 Es6 新个性的问题,其实就把 Es6 代码转换为 Es5 的代码,兼容所有浏览器,babel 转换代码其实就是用了 AST,babel 与 AST 就有着很一种特地的关系。

那么咱们就在 babel 的中来应用 AST,看看 babel 是如何编译代码的(不讲源码啊)

须要用到两个工具包 @babel/core@babel/preset-env

当咱们配置 babel 的时候,不论是在 .babelrc 或者 babel.config.js 文件外面配置的都有 presetsplugins 两个配置项(还有其余配置项,这里不做介绍)

插件和预设的区别

// .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。

babel 插件的应用

当初咱们有一个箭头函数,要想把它转成一般函数,咱们就能够间接这么写:

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 构造

首先咱们在在线剖析 AST 的网站上剖析 const fn = (a, b) => a + bconst fn = function(a, b) {return a + b}看两者语法树的区别

依据咱们剖析可得:

  1. 变成一般函数之后他就不叫箭头函数了 ArrowFunctionExpression,而是函数表达式了 FunctionExpression
  2. 所以首先咱们要把 箭头函数表达式 (ArrowFunctionExpression) 转换为 函数表达式(FunctionExpression)
  3. 要把 二进制表达式 (BinaryExpression) 包裹在 返回语句中 (ReturnStatement) 而后 push 到 代码块中(BlockStatement)
  4. 其实咱们要做就是把一棵树变成另外一颗树,说白了其实就是拼成另一颗树的构造,而后生成新的代码,就能够实现代码的转换

访问者模式

在 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);

批改 AST 构造

此时咱们拿到的后果是这样的节点后果是 这样的,其实就是 ArrowFunctionExpression 的 AST,此时咱们要做的是把 ArrowFunctionExpression 的构造替换成 FunctionExpression的构造,然而须要咱们组装相似的构造,这么间接写很麻烦,然而 babel 为咱们提供了一个工具叫做 @babel/types

@babel/types 有两个作用:

  1. 判断这个节点是不是这个节点(ArrowFunctionExpression 上面的 path.node 是不是一个 ArrowFunctionExpression)
  2. 生成对应的表达式

而后咱们应用的时候,须要常常查文档,因为外面的节点类型特地多,不是做编译相干工作的是记不住怎么多节点的

那么接下来咱们就开始生成一个 FunctionExpression,而后把之前的 ArrowFunctionExpression 替换掉,咱们能够看 types 文档,找到 functionExpression,该办法承受相应的参数咱们传递过来即可生成一个 FunctionExpression

t.functionExpression(id, params, body, generator, async);
  • id: Identifier (default: null) id 可传递 null
  • params: Array<LVal> (required) 函数参数,能够把之前的参数拿过去
  • body: BlockStatement (required) 函数体,承受一个 BlockStatement 咱们须要生成一个
  • generator: boolean (default: false) 是否为 generator 函数,当然不是了
  • async: boolean (default: false) 是否为 async 函数,必定不是了

还须要生成一个 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-uivant 或者 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'

看一下两个语法树的区别

依据两张图剖析咱们能够失去一些信息:

  1. 咱们发现解构形式引入的模块只有 import 申明,第二张图是两个 import 申明
  2. 解构形式引入的具体阐明外面 (specifiers) 是两个 ImportSpecifier,第二张图外面是离开的,而且都是 ImportDefaultSpecifier
  3. 他们引入的 source 也不一样
  4. 那咱们要做的其实就是要把单个的 ImportDeclaration 变成多个 ImportDeclaration, 而后把单个 import 解构引入的 specifiers 局部 ImportSpecifier 转换成多个 ImportDefaultSpecifier 并批改对应的 source 即可

剖析类型

为了不便传递参数,这次咱们写到一个函数外面,能够不便传递转换后拼接的目录

这里咱们须要用到的几个类型,也须要在 types 官网上找对应的解释

  • 首先咱们要生成多个 importDeclaration 类型

    /**
     * @param {Array<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier>} specifiers  (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);


看打印后果和转换后果仿佛没什么问题,这个插件简直就实现了

![](https://chengyuming.cn/imgs/ast-import-plugins-result_1.jpg)

### 非凡状况

然而咱们思考一种状况,如果用户不全副按需加载了,按需加载只是一种抉择,如果用户这么写了 `import vant, {Button, Icon} from 'vant'`,那么咱们这个插件就呈现问题了

![](https://chengyuming.cn/imgs/ast-import-plugins-result_2.jpg)

如果遇到这种写法,那么默认导入的他的 `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);
    }
  },
},

};
}


## babylon

> 在 babel 官网上有一句话 [Babylon is a JavaScript parser used in Babel](https://babeljs.io/docs/en/babylon).

### babylon 与 babel 的关系

`babel` 应用的引擎是 `babylon`,`Babylon` 并非 `babel` 团队本人开发的,而是 fork 的 `acorn` 我的项目,`acorn` 的我的项目自己在很早之前在趣味部落 1.0 在构建中应用,为了是做一些代码的转换,是很不错的一款引擎,不过 `acorn` 引擎只提供根本的解析 `ast` 的能力,遍历还须要配套的 `acorn-travesal`, 替换节点须要应用 acorn-,而这些开发,在 Babel 的插件体系开发下,变得一体化了(摘自 AlloyTeam 团队的[分析 babel](http://www.alloyteam.com/2017/04/analysis-of-babel-babel-overview/))### 应用 babylon

应用 babylon 编写一个数组 rest 转 Es5 语法的插件

把 `const arr = [...arr1, ...arr2]` 转成 `var arr = [].concat(arr1, arr2)`

咱们应用 babylon 的话就不须要应用 `@babel/core` 了,只须要用到他外面的 `traverse` 和 `generator`,用到的包有 `babylon、@babel/traverse、@babel/generator、@babel/types`

### 剖析语法树

先来看一下两棵语法树的区别

![](https://chengyuming.cn/imgs/ast-rest-to-concat.jpg)

依据上图咱们剖析得出:1. 两棵树都是变量申明的形式,不同的是他们申明的关键字不一样
2. 他们初始化变量值的时候是不一样的,一个数组表达式(ArrayExpression)另一个是调用表达式(CallExpression)3. 那咱们要做的就很简略了,就是把 数组表达式转换为调用表达式就能够

### 剖析类型

这段代码的外围生成一个 callExpression 调用表达式,所以对应官网上的类型,咱们剖析须要用到的 api

- 先来剖析 init 外面的,首先是 callExpression

/**

  • @param {Expression} callee (required)
  • @param {Array<Expression | SpreadElement | JSXNamespacedName>} source (required)

*/
t.callExpression(callee, arguments);


- 对应语法树上 callee 是一个 MemberExpression,所以要生成一个成员表达式

/**

  • @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);


- 在 callee 的 object 是一个 ArrayExpression 数组表达式,是一个空数组

/**

  • @param {Array<null | Expression | SpreadElement>} elements (default: [])

*/
t.arrayExpression(elements);


- 对了外面的货色剖析完了,咱们还要生成 VariableDeclarator 和 VariableDeclaration 最终生成新的语法树

/**

  • @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

异步终极解决方案:`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) {}
}


### 剖析语法树

![](https://chengyuming.cn/imgs/ast-async-try-catch.jpg)

咱们发现咱们要做的就是在 `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 语句补充残缺

![](https://chengyuming.cn/imgs/ast-async-try-catch-all.jpg)

此时咱们输出的代码如下:

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 被创立进去,在后续的处理过程中,比方语义分析阶段,会增加一些信息。可参考[形象语法树和具体语法树有什么区别?](https://www.it-swarm.dev/zh/parsing/%E6%8A%BD%E8%B1%A1%E8%AF%AD%E6%B3%95%E6%A0%91%E5%92%8C%E5%85%B7%E4%BD%93%E8%AF%AD%E6%B3%95%E6%A0%91%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F/968637142/)

## 补充

对于 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 树的具体定义,可参考[这里](https://github.com/babel/babylon/blob/master/ast/spec.md)

## 代码地址

代码以寄存到 GitHub,[地址](https://github.com/fecym/ast-share.git)

## 相干链接

1. [JavaScript 语法解析、AST、V8、JIT](https://cheogo.github.io/learn-javascript/201709/runtime.html)
2. [详解 AST 形象语法树](https://blog.csdn.net/huangpb123/article/details/84799198)
3. [AST 形象语法树](https://segmentfault.com/a/1190000016706589?utm_medium=referral&utm_source=tuicool) ps: 这个外面有 class 转 Es5 构造函数的过程,有趣味能够看一下
4. [分析 Babel——Babel 总览 | AlloyTeam](http://www.alloyteam.com/2017/04/analysis-of-babel-babel-overview)
5. [不要给 async 函数写那么多 try/catch 了](https://juejin.im/post/5d25b39bf265da1bb67a4176)
6. [@babel/types](https://babeljs.io/docs/en/babel-types)
7. [文章已同步掘金](https://juejin.im/post/5eef4b416fb9a058583e7d71)

正文完
 0