一文助你搞懂AST

什麼是 AST

抽象語法樹(Abstract Syntax Tree)簡稱 AST,是源代碼的抽象語法結構的樹狀表現形式。webpackeslint 等不少工具庫的核心都是經過抽象語法書這個概念來實現對代碼的檢查、分析等操做。今天我爲你們分享一下 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、=、2web

[
  { 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

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)
   * @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);
        }
      },
    },
  };
}

babylon

在 babel 官網上有一句話 Babylon is a JavaScript parser used in Babel.

babylon 與 babel 的關係

babel 使用的引擎是 babylonBabylon 並不是 babel 團隊本身開發的,而是 fork 的 acorn 項目,acorn 的項目本人在很早以前在興趣部落 1.0 在構建中使用,爲了是作一些代碼的轉換,是很不錯的一款引擎,不過 acorn 引擎只提供基本的解析 ast 的能力,遍歷還須要配套的 acorn-travesal, 替換節點須要使用 acorn-,而這些開發,在 Babel 的插件體系開發下,變得一體化了(摘自 AlloyTeam 團隊的剖析 babel

使用 babylon

使用 babylon 編寫一個數組 rest 轉 Es5 語法的插件

const arr = [ ...arr1, ...arr2 ] 轉成 var arr = [].concat(arr1, arr2)

咱們使用 babylon 的話就不須要使用 @babel/core 了,只須要用到他裏面的 traversegenerator,用到的包有 babylon、@babel/traverse、@babel/generator、@babel/types

分析語法樹

先來看一下兩棵語法樹的區別

根據上圖咱們分析得出:

  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/catchawait 語句

// 轉換前
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,地址

相關連接

  1. JavaScript 語法解析、AST、V八、JIT
  2. 詳解 AST 抽象語法樹
  3. AST 抽象語法樹 ps: 這個裏面有 class 轉 Es5 構造函數的過程,有興趣能夠看一下
  4. 剖析 Babel——Babel 總覽 | AlloyTeam
  5. 不要給 async 函數寫那麼多 try/catch 了
  6. @babel/types
  7. 文章已同步掘金
  8. 原文地址 - AST團隊分享
相關文章
相關標籤/搜索