摘要:本文除了介紹的一些基本概念外,更偏重實戰,講解如何利用它來對代碼進行修改。二基本概念全稱,也就是抽象語法樹,它是將編程語言轉換成機器語言的橋梁。四實戰下面我們來詳細看看如何對進行操作。
歡迎關注我的公眾號睿Talk,獲取我最新的文章:
最近突然對 AST 產生了興趣,深入了解后發現它的使用場景還真的不少,很多我們日常開發使用的工具都跟它息息相關,如 Babel、ESLint 和 Prettier 等。本文除了介紹 AST 的一些基本概念外,更偏重實戰,講解如何利用它來對代碼進行修改。
二、基本概念AST 全稱 Abstract Syntax Tree,也就是抽象語法樹,它是將編程語言轉換成機器語言的橋梁。瀏覽器在解析 JS 的過程中,會根據 ECMAScript 標準將字符串進行分詞,拆分為一個個語法單元。然后再遍歷這些語法單元,進行語義分析,構造出 AST。最后再使用 JIT 編譯器的全代碼生成器,將 AST 轉換為本地可執行的機器碼。如下面一段代碼:
function add(a, b) { return a + b; }
進行分詞后,會得到這些 token:
對 token 進行分析,最終會得到這樣一棵 AST(簡化版):
{ "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 后就可以根據規則轉換為機器碼了,在此不再贅述。
三、Babel 工作原理AST 除了可以轉換為機器碼外,還能做很多事情,如 Babel 就能通過分析 AST,將 ES6 的代碼轉換成 ES5。
Babel 的編譯過程分為 3 個階段:
解析:將代碼字符串解析成抽象語法樹
變換:對抽象語法樹進行變換操作
生成:根據變換后的抽象語法樹生成新的代碼字符串
Babel 實現了一個 JS 版本的解析器Babel parser,它能將 JS 字符串轉換為 JSON 結構的 AST。為了方便對這棵樹進行遍歷和變換操作,babel 又提供了traverse工具函數。完成 AST 的修改后,可以使用generator生成新的代碼。
四、AST 實戰下面我們來詳細看看如何對 AST 進行操作。先建好如下的代碼模板:
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);
構造一個 hello world
打開 AST Explorer,將左側代碼清空,再輸入 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" }
接下來通過代碼構造這個ExpressionStatement:
const code = ``; const ast = parser.parse(code); // 生成 literal const literal = t.stringLiteral("hello world") // 生成 expressionStatement const exp = t.expressionStatement(literal) // 將表達式放入body中 ast.program.body.push(exp) const output = generate(ast, {}, code);
可以看到 AST 的創建過程就是自底向上創建各種節點的過程。這里我們借助 babel 提供的types對象幫我們創建各種類型的節點。更多類型可以查閱這里。
同樣道理,下面我們來看看如何構造一個賦值語句:
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]) // 將表達式放入body中 ast.program.body.push(declaration) const output = generate(ast, {}, code);
獲取 AST 中的節點
下面我們將對這段代碼進行操作:
export default { data() { return { count: 0 } }, methods: { add() { ++this.count }, minus() { --this.count } } }
假設我想獲取這段代碼中的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方法的第二個參數是一個對象,只要提供與節點類型同名的屬性,就能獲取到所有的這種類型的節點。通過path參數能訪問到節點信息,進而找出需要操作的節點。上面的代碼中,我們找到方法名為data的方法后,將其改名為myData,然后停止遍歷,生成新的代碼。
替換 AST 中的節點
可以使用replaceWith和replaceWithSourceString替換節點,例子如下:
// 將 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")) ); // 下面的操作跟上一條語句等價,更加直觀方便 // path.get("object").replaceWithSourceString("this.data"); } } }); const output = generate(ast, {}, code);
插入新的節點
可以使用pushContainer、insertBefore和insertAfter等方法來插入節點:
// 這個例子示范了 3 種節點插入的方法 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);
刪除節點
使用remove方法來刪除節點:
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);五、總結
本文介紹了 AST 的一些基本概念,講解了如何使用 Babel 提供的 API,對 AST 進行增刪改查的操作。?掌握這項技能,再加上一點想象力,就能制作出實用的代碼分析和轉換工具。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/105902.html
摘要:生成屬性這一步,我們要先提取原函數中的的對象。所以這里我們還是主要使用來訪問節點獲取第一級的,也就是函數體將合并的寫法用生成生成生成插入到原函數下方刪除原函數程序輸出將中的屬性提升一級這里遍歷中的屬性沒有再采用,因為這里結構是固定的。 ??經過之前的三篇文章介紹,AST的CRUD都已經完成。下面主要通過vue轉小程序過程中需要用到的部分關鍵技術來實戰。 下面的例子的核心代碼依然是最簡單...
摘要:操作通常配合來完成。因為是個數組,因此,我們可以直接使用數組操作自我毀滅方法極為簡單,找到要刪除的,執行就結束了。如上述代碼,我們要刪除屬性,代碼如下到目前為止,的我們都介紹完了,下面一篇文章以轉小程序為例,我們來實戰一波。 ??通過前兩篇文章的介紹,大家已經了解了Create和Retrieve,我們接著介紹Update和 Remove操作。Update操作通常配合Create來完成。...
摘要:抽象語法樹,是一個非常基礎而重要的知識點,但國內的文檔卻幾乎一片空白。事實上,在世界中,你可以認為抽象語法樹是最底層。通過抽象語法樹解析,我們可以像童年時拆解玩具一樣,透視這臺機器的運轉,并且重新按著你的意愿來組裝。 抽象語法樹(AST),是一個非常基礎而重要的知識點,但國內的文檔卻幾乎一片空白。本文將帶大家從底層了解AST,并且通過發布一個小型前端工具,來帶大家了解AST的強大功能 ...
摘要:超出此時間則渲染錯誤組件。元素節點總共有種類型,為表示是普通元素,為表示是表達式,為表示是純文本。 實戰 - 插件 form-validate {{ error }} ...
實踐是所有展示最好的方法,因此我覺得可以不必十分細致的,但我們的展示卻是整體的流程、輸入和輸出。現在我們就看看Vue 的指令、內置組件等。也就是第二篇,模型樹優化。 分析了 Vue 編譯三部曲的第一步,「如何將 template 編譯成 AST ?」上一篇已經介紹,但我們還是來總結回顧下,parse 的目的是將開發者寫的 template 模板字符串轉換成抽象語法樹 AST ,AST 就這里...
閱讀 3128·2021-11-15 18:14
閱讀 1779·2021-09-22 10:51
閱讀 3291·2021-09-09 09:34
閱讀 3509·2021-09-06 15:02
閱讀 1026·2021-09-01 11:40
閱讀 3193·2019-08-30 13:58
閱讀 2531·2019-08-30 11:04
閱讀 1087·2019-08-28 18:31