本文首發於 hzzly的博客javascript
原文連接:AST的簡單實踐前端
It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code.vue
AST是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。java
AST是一個很是基礎可是同時很是重要的知識點,咱們熟知的 TypeScript、babel、webpack、vue-cli 都是依賴 AST 進行開發的。node
這裏咱們就以 babel 爲例來實踐一下 AST。webpack
Babel 做爲當今最爲經常使用的 JavaScript 編譯器,在前端開發中扮演着極爲重要的角色。大多數狀況下,Babel 被用來轉譯 ECMAScript 2015+ 至可兼容瀏覽器的版本。web
Babel 的三個主要處理步驟分別是:vue-cli
整個過程當中,parsing和generation是固定不變的,最關鍵的是transforming步驟,經過babel插件來支持,這是其擴展性的關鍵。npm
這三個階段分別由 @babel/parser、@babel/core、@babel/generator 執行。Babel 本質上只是一個代碼的搬運工,若是不給 Babel 裝上插件,它將會把輸入的代碼原封不動地輸出。正是由於有插件的存在, Babel 才能將輸入的代碼進行轉變,從而生成新的代碼。編程
輸入JS源碼,輸出AST
parsing(解析),對應於編譯器的詞法分析,及語法分析階段。輸入的源碼字符序列通過詞法分析,生成具備詞法意義的token序列(可以區分出關鍵字、數值、標點符號等),接着通過語法分析,生成具備語法意義的AST(可以區分出語句塊、註釋、變量聲明、函數參數等)。
利用 @babel/parser 對源代碼進行解析 獲得 AST。
慄如:
console.log(info)
複製代碼
通過parsing後,生成的AST以下:
{
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"loc": {
"identifierName": "console",
},
"name": "console",
},
"property": {
"type": "Identifier",
"loc": {
"identifierName": "log",
},
"name": "log",
}
},
"arguments": [
"Identifier": {
"type": "Identifier",
"loc": {
"identifierName": "log",
},
"name": "info",
}
]
}
複製代碼
🔥Tip: JS代碼對應的AST結構能夠經過AST Explorer工具查看
仔細的小夥伴可能就會發現從咱們的源代碼到AST的過程其實就是一個分詞的過程,將咱們的 console.log(info) 分紅 console、log、info。
有了這個 AST 樹結構,咱們就能進行語義層面轉換了。
輸入AST,輸出修改過的AST
利用 @babel/traverse 對 AST 進行遍歷,並解析出整個樹的 path,經過掛載的 metadataVisitor 讀取對應的元信息,這一步叫 set AST 過程。
@babel/traverse 是一款用來自動遍歷抽象語法樹的工具,它會訪問樹中的全部節點,在進入每一個節點時觸發 enter 鉤子函數,退出每一個節點時觸發 exit 鉤子函數。開發者可在鉤子函數中對 AST 進行修改。
import traverse from "@babel/traverse";
traverse(ast, {
enter(path) {
// 進入 path 後觸發
},
exit(path) {
// 退出 path 前觸發
},
});
複製代碼
transforming(轉換),對應於編譯器的機器無關代碼優化階段(稍微有點牽強,但兩者工做內容都是修改AST),對 AST 作一些修改,好比針對上面的 log 增長一些信息方便咱們調試:
console.log(info) => console.log('[info]', info)
複製代碼
修改事後的 AST 結構:
{
"type": "CallExpression",
"callee": {
// ....
},
"arguments": [
"StringLiteral": {
"type": "StringLiteral",
"value": "'[info]'",
},
"Identifier": {
"type": "Identifier",
"loc": {
"identifierName": "log",
},
"name": "info",
}
]
}
複製代碼
語義層面的轉換具體而言就是對AST進行增、刪、改操做,修改後的AST可能具備不一樣的語義,映射回代碼字符串也不一樣
輸入AST,輸出JS源碼
generation(生成),對應於編譯器的代碼生成階段,把AST映射回代碼字符串。
利用 @babel/generator 將 AST 樹輸出爲轉碼後的代碼字符串。
說了這麼多接下來咱們就用代碼實踐一下上面的例子
const parser = require('@babel/parser');
const traverse = require('@babel/traverse');
const generate = require('@babel/generator');
const t = require('@babel/types');
function compile(code) {
// 1. parse
const ast = parser.parse(code);
// 2. traverse
const visitor = {
CallExpression(path) {
const { callee, arguments } = path.node;
if (
t.isMemberExpression(callee)
&& callee.object.name === 'console'
&& callee.property.name === 'log'
&& arguments.length > 0
) {
const variableName = arguments[0].name;
path.node.arguments.unshift(
t.StringLiteral(`[${variableName}]`)
)
}
},
};
traverse.default(ast, visitor);
// 3. generate
return generate.default(ast, {}, code);
}
const code = `console.log(info)`;
const result = compile(code);
console.log(result.code);
複製代碼
看到這,咱們的 AST 實踐也告一段落了。固然,文章所講的只是一個簡單的例子,但基本的原理思路八九不離十,更多的類型還得本身去探究。總之,掌握好 AST,你真的能夠作不少事情。