摘要:本文除了介紹的一些基本概念外,更偏重實(shí)戰(zhàn),講解如何利用它來對(duì)代碼進(jìn)行修改。二基本概念全稱,也就是抽象語(yǔ)法樹,它是將編程語(yǔ)言轉(zhuǎn)換成機(jī)器語(yǔ)言的橋梁。四實(shí)戰(zhàn)下面我們來詳細(xì)看看如何對(duì)進(jìn)行操作。
歡迎關(guān)注我的公眾號(hào)睿Talk,獲取我最新的文章:
最近突然對(duì) AST 產(chǎn)生了興趣,深入了解后發(fā)現(xiàn)它的使用場(chǎng)景還真的不少,很多我們?nèi)粘i_發(fā)使用的工具都跟它息息相關(guān),如 Babel、ESLint 和 Prettier 等。本文除了介紹 AST 的一些基本概念外,更偏重實(shí)戰(zhàn),講解如何利用它來對(duì)代碼進(jìn)行修改。
二、基本概念AST 全稱 Abstract Syntax Tree,也就是抽象語(yǔ)法樹,它是將編程語(yǔ)言轉(zhuǎn)換成機(jī)器語(yǔ)言的橋梁。瀏覽器在解析 JS 的過程中,會(huì)根據(jù) ECMAScript 標(biāo)準(zhǔn)將字符串進(jìn)行分詞,拆分為一個(gè)個(gè)語(yǔ)法單元。然后再遍歷這些語(yǔ)法單元,進(jìn)行語(yǔ)義分析,構(gòu)造出 AST。最后再使用 JIT 編譯器的全代碼生成器,將 AST 轉(zhuǎn)換為本地可執(zhí)行的機(jī)器碼。如下面一段代碼:
function add(a, b) { return a + b; }
進(jìn)行分詞后,會(huì)得到這些 token:
對(duì) token 進(jìn)行分析,最終會(huì)得到這樣一棵 AST(簡(jiǎn)化版):
{ "type": "Program", "body": [ { "type": "FunctionDeclaration", "id": { "type": "Identifier", "name": "add" }, "params": [ { "type": "Identifier", "name": "a" }, { "type": "Identifier", "name": "b" } ], "body": { "type": "BlockStatement", "body": [ { "type": "ReturnStatement", "argument": { "type": "BinaryExpression", "left": { "type": "Identifier", "name": "a" }, "operator": "+", "right": { "type": "Identifier", "name": "b" } } } ] } } ], "sourceType": "module" }
拿到 AST 后就可以根據(jù)規(guī)則轉(zhuǎn)換為機(jī)器碼了,在此不再贅述。
三、Babel 工作原理AST 除了可以轉(zhuǎn)換為機(jī)器碼外,還能做很多事情,如 Babel 就能通過分析 AST,將 ES6 的代碼轉(zhuǎn)換成 ES5。
Babel 的編譯過程分為 3 個(gè)階段:
解析:將代碼字符串解析成抽象語(yǔ)法樹
變換:對(duì)抽象語(yǔ)法樹進(jìn)行變換操作
生成:根據(jù)變換后的抽象語(yǔ)法樹生成新的代碼字符串
Babel 實(shí)現(xiàn)了一個(gè) JS 版本的解析器Babel parser,它能將 JS 字符串轉(zhuǎn)換為 JSON 結(jié)構(gòu)的 AST。為了方便對(duì)這棵樹進(jìn)行遍歷和變換操作,babel 又提供了traverse工具函數(shù)。完成 AST 的修改后,可以使用generator生成新的代碼。
四、AST 實(shí)戰(zhàn)下面我們來詳細(xì)看看如何對(duì) AST 進(jìn)行操作。先建好如下的代碼模板:
import parser from "@babel/parser"; import generator from "@babel/generator"; import t from "@babel/types"; import traverser from "@babel/traverse"; const generate = generator.default; const traverse = traverser.default; const code = ``; const ast = parser.parse(code); // AST 變換 const output = generate(ast, {}, code); console.log("Input ", code); console.log("Output ", output.code);
構(gòu)造一個(gè) hello world
打開 AST Explorer,將左側(cè)代碼清空,再輸入 hello world,可以看到前后 AST 的樣子:
// 空 { "type": "Program", "body": [], "sourceType": "module" } // hello world { "type": "Program", "body": [ { "type": "ExpressionStatement", "expression": { "type": "Literal", "value": "hello world", "raw": ""hello world"" }, "directive": "hello world" } ], "sourceType": "module" }
接下來通過代碼構(gòu)造這個(gè)ExpressionStatement:
const code = ``; const ast = parser.parse(code); // 生成 literal const literal = t.stringLiteral("hello world") // 生成 expressionStatement const exp = t.expressionStatement(literal) // 將表達(dá)式放入body中 ast.program.body.push(exp) const output = generate(ast, {}, code);
可以看到 AST 的創(chuàng)建過程就是自底向上創(chuàng)建各種節(jié)點(diǎn)的過程。這里我們借助 babel 提供的types對(duì)象幫我們創(chuàng)建各種類型的節(jié)點(diǎn)。更多類型可以查閱這里。
同樣道理,下面我們來看看如何構(gòu)造一個(gè)賦值語(yǔ)句:
const code = ``; const ast = parser.parse(code); // 生成 identifier const id = t.identifier("str") // 生成 literal const literal = t.stringLiteral("hello world") // 生成 variableDeclarator const declarator = t.variableDeclarator(id, literal) // 生成 variableDeclaration const declaration = t.variableDeclaration("const", [declarator]) // 將表達(dá)式放入body中 ast.program.body.push(declaration) const output = generate(ast, {}, code);
獲取 AST 中的節(jié)點(diǎn)
下面我們將對(duì)這段代碼進(jìn)行操作:
export default { data() { return { count: 0 } }, methods: { add() { ++this.count }, minus() { --this.count } } }
假設(shè)我想獲取這段代碼中的data方法,可以直接這么訪問:
const dataProperty = ast.program.body[0].declaration.properties[0]
也可以使用 babel 提供的traverse工具方法:
const code = ` export default { data() { return { count: 0 } }, methods: { add() { ++this.count }, minus() { --this.count } } } `; const ast = parser.parse(code, {sourceType: "module"}); // const dataProperty = ast.program.body[0].declaration.properties[0] traverse(ast, { ObjectMethod(path) { if (path.node.key.name === "data") { path.node.key.name = "myData"; // 停止遍歷 path.stop(); } } }) const output = generate(ast, {}, code);
traverse方法的第二個(gè)參數(shù)是一個(gè)對(duì)象,只要提供與節(jié)點(diǎn)類型同名的屬性,就能獲取到所有的這種類型的節(jié)點(diǎn)。通過path參數(shù)能訪問到節(jié)點(diǎn)信息,進(jìn)而找出需要操作的節(jié)點(diǎn)。上面的代碼中,我們找到方法名為data的方法后,將其改名為myData,然后停止遍歷,生成新的代碼。
替換 AST 中的節(jié)點(diǎn)
可以使用replaceWith和replaceWithSourceString替換節(jié)點(diǎn),例子如下:
// 將 this.count 改成 this.data.count const code = `this.count`; const ast = parser.parse(code); traverse(ast, { MemberExpression(path) { if ( t.isThisExpression(path.node.object) && t.isIdentifier(path.node.property, { name: "count" }) ) { // 將 this 替換為 this.data path .get("object") .replaceWith( t.memberExpression(t.thisExpression(), t.identifier("data")) ); // 下面的操作跟上一條語(yǔ)句等價(jià),更加直觀方便 // path.get("object").replaceWithSourceString("this.data"); } } }); const output = generate(ast, {}, code);
插入新的節(jié)點(diǎn)
可以使用pushContainer、insertBefore和insertAfter等方法來插入節(jié)點(diǎn):
// 這個(gè)例子示范了 3 種節(jié)點(diǎn)插入的方法 const code = ` const obj = { count: 0, message: "hello world" } `; const ast = parser.parse(code); const property = t.objectProperty( t.identifier("new"), t.stringLiteral("new property") ); traverse(ast, { ObjectExpression(path) { path.pushContainer("properties", property); // path.node.properties.push(property); } }); /* traverse(ast, { ObjectProperty(path) { if ( t.isIdentifier(path.node.key, { name: "message" }) ) { path.insertAfter(property); } } }); */ const output = generate(ast, {}, code);
刪除節(jié)點(diǎn)
使用remove方法來刪除節(jié)點(diǎn):
const code = ` const obj = { count: 0, message: "hello world" } `; const ast = parser.parse(code); traverse(ast, { ObjectProperty(path) { if ( t.isIdentifier(path.node.key, { name: "message" }) ) { path.remove(); } } }); const output = generate(ast, {}, code);五、總結(jié)
本文介紹了 AST 的一些基本概念,講解了如何使用 Babel 提供的 API,對(duì) AST 進(jìn)行增刪改查的操作。?掌握這項(xiàng)技能,再加上一點(diǎn)想象力,就能制作出實(shí)用的代碼分析和轉(zhuǎn)換工具。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/105902.html
摘要:生成屬性這一步,我們要先提取原函數(shù)中的的對(duì)象。所以這里我們還是主要使用來訪問節(jié)點(diǎn)獲取第一級(jí)的,也就是函數(shù)體將合并的寫法用生成生成生成插入到原函數(shù)下方刪除原函數(shù)程序輸出將中的屬性提升一級(jí)這里遍歷中的屬性沒有再采用,因?yàn)檫@里結(jié)構(gòu)是固定的。 ??經(jīng)過之前的三篇文章介紹,AST的CRUD都已經(jīng)完成。下面主要通過vue轉(zhuǎn)小程序過程中需要用到的部分關(guān)鍵技術(shù)來實(shí)戰(zhàn)。 下面的例子的核心代碼依然是最簡(jiǎn)單...
摘要:操作通常配合來完成。因?yàn)槭莻€(gè)數(shù)組,因此,我們可以直接使用數(shù)組操作自我毀滅方法極為簡(jiǎn)單,找到要?jiǎng)h除的,執(zhí)行就結(jié)束了。如上述代碼,我們要?jiǎng)h除屬性,代碼如下到目前為止,的我們都介紹完了,下面一篇文章以轉(zhuǎn)小程序?yàn)槔?,我們來?shí)戰(zhàn)一波。 ??通過前兩篇文章的介紹,大家已經(jīng)了解了Create和Retrieve,我們接著介紹Update和 Remove操作。Update操作通常配合Create來完成。...
摘要:抽象語(yǔ)法樹,是一個(gè)非常基礎(chǔ)而重要的知識(shí)點(diǎn),但國(guó)內(nèi)的文檔卻幾乎一片空白。事實(shí)上,在世界中,你可以認(rèn)為抽象語(yǔ)法樹是最底層。通過抽象語(yǔ)法樹解析,我們可以像童年時(shí)拆解玩具一樣,透視這臺(tái)機(jī)器的運(yùn)轉(zhuǎn),并且重新按著你的意愿來組裝。 抽象語(yǔ)法樹(AST),是一個(gè)非?;A(chǔ)而重要的知識(shí)點(diǎn),但國(guó)內(nèi)的文檔卻幾乎一片空白。本文將帶大家從底層了解AST,并且通過發(fā)布一個(gè)小型前端工具,來帶大家了解AST的強(qiáng)大功能 ...
摘要:超出此時(shí)間則渲染錯(cuò)誤組件。元素節(jié)點(diǎn)總共有種類型,為表示是普通元素,為表示是表達(dá)式,為表示是純文本。 實(shí)戰(zhàn) - 插件 form-validate {{ error }} ...
實(shí)踐是所有展示最好的方法,因此我覺得可以不必十分細(xì)致的,但我們的展示卻是整體的流程、輸入和輸出?,F(xiàn)在我們就看看Vue 的指令、內(nèi)置組件等。也就是第二篇,模型樹優(yōu)化?! 》治隽?Vue 編譯三部曲的第一步,「如何將 template 編譯成 AST ?」上一篇已經(jīng)介紹,但我們還是來總結(jié)回顧下,parse 的目的是將開發(fā)者寫的 template 模板字符串轉(zhuǎn)換成抽象語(yǔ)法樹 AST ,AST 就這里...
閱讀 3150·2021-11-15 18:14
閱讀 1804·2021-09-22 10:51
閱讀 3328·2021-09-09 09:34
閱讀 3541·2021-09-06 15:02
閱讀 1066·2021-09-01 11:40
閱讀 3217·2019-08-30 13:58
閱讀 2552·2019-08-30 11:04
閱讀 1120·2019-08-28 18:31