摘要:抽象語法樹是怎么生成的談到這點(diǎn),就要說到計(jì)算機(jī)是怎么讀懂我們的代碼的。需要注意什么狀態(tài)狀態(tài)是抽象語法樹轉(zhuǎn)換的敵人,狀態(tài)管理會(huì)不斷牽扯我們的精力,而且?guī)缀跛心銓?duì)狀態(tài)的假設(shè),總是會(huì)有一些未考慮到的語法最終證明你的假設(shè)是錯(cuò)誤的。
現(xiàn)在談到 babel 肯定大家都不會(huì)感覺到陌生,雖然日常開發(fā)中很少會(huì)直接接觸到它,但它已然成為了前端開發(fā)中不可或缺的工具,不僅可以讓開發(fā)者可以立即使用 ES 規(guī)范中的最新特性,也大大的提高了前端新技術(shù)的普及(學(xué)不動(dòng)了...)。但是對(duì)于其轉(zhuǎn)換代碼的內(nèi)部原理我們大多數(shù)人卻知之甚少,所以帶著好奇與疑問,筆者嘗試對(duì)其原理進(jìn)行探索。
Babel 是一個(gè)通用的多功能 JavaScript 編譯器,但與一般編譯器不同的是它只是把同種語言的高版本規(guī)則轉(zhuǎn)換為低版本規(guī)則,而不是輸出另一種低級(jí)機(jī)器可識(shí)別的代碼,并且在依賴不同的拓展插件下可用于不同形式的靜態(tài)分析。(靜態(tài)分析:指在不需要執(zhí)行代碼的前提下對(duì)代碼進(jìn)行分析以及相應(yīng)處理的一個(gè)過程,主要應(yīng)用于語法檢查、編譯、代碼高亮、代碼轉(zhuǎn)換、優(yōu)化、壓縮等等)
babel 做了什么和編譯器類似,babel 的轉(zhuǎn)譯過程也分為三個(gè)階段,這三步具體是:
解析 Parse
將代碼解析生成抽象語法樹( 即AST ),也就是計(jì)算機(jī)理解我們代碼的方式(擴(kuò)展:一般來說每個(gè) js 引擎都有自己的 AST,比如熟知的 v8,chrome 瀏覽器會(huì)把 js 源碼轉(zhuǎn)換為抽象語法樹,再進(jìn)一步轉(zhuǎn)換為字節(jié)碼或機(jī)器代碼),而 babel 則是通過 babylon 實(shí)現(xiàn)的 。簡(jiǎn)單來說就是一個(gè)對(duì)于 JS 代碼的一個(gè)編譯過程,進(jìn)行了詞法分析與語法分析的過程。
轉(zhuǎn)換 Transform
對(duì)于 AST 進(jìn)行變換一系列的操作,babel 接受得到 AST 并通過 babel-traverse 對(duì)其進(jìn)行遍歷,在此過程中進(jìn)行添加、更新及移除等操作。
生成 Generate
將變換后的 AST 再轉(zhuǎn)換為 JS 代碼, 使用到的模塊是 babel-generator。
而 babel-core 模塊則是將三者結(jié)合使得對(duì)外提供的API做了一個(gè)簡(jiǎn)化。
此外需要注意的是,babel 只是轉(zhuǎn)譯新標(biāo)準(zhǔn)引入的語法,比如ES6箭頭函數(shù):而新標(biāo)準(zhǔn)引入的新的原生對(duì)象,部分原生對(duì)象新增的原型方法,新增的 API 等(Proxy、Set 等), 這些事不會(huì)轉(zhuǎn)譯的,需要引入對(duì)應(yīng)的 polyfill 來解決。
而我們編寫的 babel 插件則主要專注于第二步轉(zhuǎn)換過程的工作,專注于對(duì)于代碼的轉(zhuǎn)化規(guī)則的拓展,解析與生成的偏底層相關(guān)操作則有對(duì)應(yīng)的模塊支持,在此我們理解它主要做了什么即可。
比如這樣一段代碼:
console.log("hello")
則會(huì)得到這樣一個(gè)樹形結(jié)構(gòu)(已簡(jiǎn)化):
{ "type": "Program", // 程序根節(jié)點(diǎn) "body": [ { "type": "ExpressionStatement", // 一個(gè)語句節(jié)點(diǎn) "expression": { "type": "CallExpression", // 一個(gè)函數(shù)調(diào)用表達(dá)式節(jié)點(diǎn) "callee": { "type": "MemberExpression", // 表達(dá)式 "object": { "type": "Identifier", "name": "console" }, "property": { "type": "Identifier", "name": "log" }, "computed": false }, "arguments": [ { "type": "StringLiteral", "extra": { "rawValue": "hello", "raw": ""hello"" }, "value": "hello" } ] } } ], "directives": [] }
其中的所有節(jié)點(diǎn)名詞,均來源于 ECMA 規(guī)范 。
抽象語法樹是怎么生成的談到這點(diǎn),就要說到計(jì)算機(jī)是怎么讀懂我們的代碼的。解析過程分為兩個(gè)步驟:
1.分詞: 將整個(gè)代碼字符串分割成語法單元數(shù)組(token)
JS 代碼中的語法單元主要指如標(biāo)識(shí)符(if/else、return、function)、運(yùn)算符、括號(hào)、數(shù)字、字符串、空格等等能被解析的最小單元。比如下面的代碼生成的語法單元數(shù)組如下:
在線分詞工具
function demo (a) { console.log(a || "a"); } => [ { "type": "Keyword","value": "function" }, { "type": "Identifier","value": "demo" }, { "type": "Punctuator","value": "(" }, { "type": "Identifier","value": "a" }, { "type": "Punctuator","value": ")" }, { "type": "Punctuator","value": "{ " }, { "type": "Identifier","value": "console" }, { "type": "Punctuator","value": "." }, { "type": "Identifier","value": "log" }, { "type": "Punctuator","value": "(" }, { "type": "Identifier","value": "a" }, { "type": "Punctuator","value": "||" }, { "type": "String","value": ""a"" }, { "type": "Punctuator","value": ")" }, { "type": "Punctuator","value": "}" } ]
2.語義分析: 在分詞結(jié)果的基礎(chǔ)上分析語法單元之間的關(guān)系。
語義分析則是將得到的詞匯進(jìn)行一個(gè)立體的組合,確定詞語之間的關(guān)系。考慮到編程語言的各種從屬關(guān)系的復(fù)雜性,語義分析的過程又是在遍歷得到的語法單元組,相對(duì)而言就會(huì)變得更復(fù)雜。
先理解兩個(gè)重要概念,即語句和表達(dá)式。
語句(statement),即指一個(gè)具備邊界的代碼區(qū)域,相鄰的兩個(gè)語句之間從語法上來講互補(bǔ)影響,即調(diào)換順序也不會(huì)產(chǎn)生語法錯(cuò)誤。
表達(dá)式(expression),則指最終有個(gè)結(jié)果的一小段代碼,他可以嵌入到另一個(gè)表達(dá)式,且包含在語句中。
簡(jiǎn)單來說語義分析既是對(duì)語句和表達(dá)式識(shí)別,這是個(gè)遞歸過程,在解析中,babel 會(huì)在解析每個(gè)語句和表達(dá)式的過程中設(shè)置一個(gè)暫存器,用來暫存當(dāng)前讀取到的語法單元,如果解析失敗,就會(huì)返回之前的暫存點(diǎn),再按照另一種方式進(jìn)行解析,如果解析成功,則將暫存點(diǎn)銷毀,不斷重復(fù)以上操作,直到最后生成對(duì)應(yīng)的語法樹。
{"type": "Program", "body": [{ "type": "FunctionDeclaration", "id": { "type": "Identifier", "name": "demo" }, "params": [{ "type": "Identifier", "name": "a" }], "body": { "type": "BlockStatement", "body": [{ "type": "ExpressionStatement", "expression": { "type": "CallExpression", "callee": { "type": "MemberExpression", "computed": false, "object": { "type": "Identifier", "name": "console" }, "property": { "type": "Identifier", "name": "log" } }, "arguments": [{ "type": "LogicalExpression", "operator": "||", "left": { "type": "Identifier", "name": "a" }, "right": { "type": "Literal", "value": "a", "raw": ""a"" } }] } }] }, }]}
推薦
the-super-tiny-compiler 這是一個(gè)只用了百來行代碼的簡(jiǎn)單編譯器開源項(xiàng)目,里面的作者也很用心的編寫了詳盡的注釋,通過代碼可以更好地理解這個(gè)過程。
了解源代碼的 AST 結(jié)構(gòu)則是我們轉(zhuǎn)換過程的關(guān)鍵點(diǎn),可以借助直觀的樹形結(jié)構(gòu)轉(zhuǎn)換 AST Explorer,更加直觀的理解 AST 結(jié)構(gòu)。
Visitors
對(duì)于這個(gè)遍歷過程,babel 通過實(shí)例化 visitor 對(duì)象完成,既其實(shí)我們生成出來的 AST 結(jié)構(gòu)都擁有一個(gè) accept 方法用來接收 visitor 訪問者對(duì)象的訪問,而訪問者其中也定義了 visit 方法(即開發(fā)者定義的函數(shù)方法)使其能夠?qū)錉罱Y(jié)構(gòu)不同節(jié)點(diǎn)做出不同的處理,借此做到在對(duì)象結(jié)構(gòu)的一次訪問過程中,我們能夠遍歷整個(gè)對(duì)象結(jié)構(gòu)。(訪問者設(shè)計(jì)模式:提供一個(gè)作用于某對(duì)象結(jié)構(gòu)中的各元素的操作表示,它使得可以在不改變各元素的類的前提下定義作用于這些元素的新操作)
遍歷結(jié)點(diǎn)讓我們可以定位并找到我們想要操作的結(jié)點(diǎn),在遍歷每一個(gè)節(jié)點(diǎn)時(shí),存在enter和exit兩個(gè)時(shí)態(tài)周期,一個(gè)是進(jìn)入結(jié)點(diǎn)時(shí),這個(gè)時(shí)候節(jié)點(diǎn)的子節(jié)點(diǎn)還沒觸達(dá),遍歷子節(jié)點(diǎn)完成的后,會(huì)離開該節(jié)點(diǎn)并觸發(fā)exit方法。
Paths
Visitors 在遍歷到每個(gè)節(jié)點(diǎn)的時(shí)候,都會(huì)給我們傳入 path 參數(shù),包含了節(jié)點(diǎn)的信息以及節(jié)點(diǎn)和所在的位置,供我們對(duì)特定節(jié)點(diǎn)進(jìn)行修改,之所以稱之為 path 是其表示的是兩個(gè)節(jié)點(diǎn)之間連接的對(duì)象,而非指當(dāng)前的節(jié)點(diǎn)對(duì)象。path屬性有幾個(gè)重要的組成,主要如下:
例如,如果訪問到下面這樣的一個(gè)節(jié)點(diǎn)
{ type: "FunctionDeclaration", id: { type: "Identifier", name: "square" } }
而他的 path 關(guān)聯(lián)路徑得到的對(duì)象則是這樣的。
{ "parent": { "type": "FunctionDeclaration", "id": {...},... }, { "node": { "type": "Identifier", "name": "square" } } }
可以看到 path 其實(shí)是一個(gè)節(jié)點(diǎn)在樹中的位置以及關(guān)于該節(jié)點(diǎn)各種信息的響應(yīng)式表示,即我們?cè)L問過程中操作的并不是節(jié)點(diǎn)本身而是路徑,且其中包含了添加、更新、移動(dòng)和刪除節(jié)點(diǎn)有關(guān)的其他很多方法,當(dāng)調(diào)用一個(gè)修改樹的方法后,路徑信息也會(huì)被更新。主要目的還是為了簡(jiǎn)化操作,盡可能做到無狀態(tài)。
實(shí)際運(yùn)用
假如有如下代碼:
NEJ.define(["./modal"], function(Modal){}); => transform 為 define(["./modal"], function(Modal){});
我們想要把 NEJ.define轉(zhuǎn)化為 define,為了將模塊依賴系統(tǒng)轉(zhuǎn)換為標(biāo)準(zhǔn)的 AMD 形式,則可以用編寫 babel 插件的方式去做。
首先我們先分析需要訪問修改的 AST 結(jié)構(gòu)
{ ExpressionStatement { expression: CallExpression { callee: MemberExpression { object: Identifier { name: "NEJ" } property: Identifier { name: "define" } } arguments: [ ArrayExpression{}, FunctionExpression{} ] } } } => 轉(zhuǎn)化為下面這樣 { ExpressionStatement { expression: CallExpression { callee: Identifier { name: "define" } arguments: [ ArrayExpression{}, FunctionExpression{} ] } } }
分析結(jié)構(gòu)可以看到,arguments 是代碼中傳入的參數(shù)部分,這部分保持不變直接拿到就可以了,我們需要修改的是 MemberExpression 表達(dá)式節(jié)點(diǎn)下的name 為 "NEJ" 的 Identifier部分,由于修改后的結(jié)構(gòu)是一個(gè)CallExpression函數(shù)調(diào)用形式的表達(dá)式,那么整體思路現(xiàn)在就是創(chuàng)建一個(gè)CallExpression替換掉原來的 MemberExpression即可。這里借用了 babel-type( 為 babel提供多種輔助函數(shù),類似于 loadsh 與 js之間的關(guān)系)創(chuàng)建節(jié)點(diǎn)。
const babel = require("babel-core"); const t = require("babel-types"); const code = "NEJ.define(["./modal"], function(Modal){});"; let args = []; const visitor = { ExpressionStatement(path) { if (path.node && path.node.arguments) { args = path.node.arguments; } }, MemberExpression(path) { if (path.node && path.node.object && path.node.object.name === "NEJ") { path.replaceWith(t.CallExpression( t.identifier("define"), args )) } } } const result = babel.transform(code, { plugins: [{ visitor }] }) console.log(result.code)
執(zhí)行后即可看到結(jié)果
define((["./modal"], function (Modal) {});
在代碼中可以看到,對(duì)于每一步訪問到的節(jié)點(diǎn)我們都要嚴(yán)格的判斷是否與我們預(yù)想的類型一致,這樣不僅是為了排除到其他情況,更是為了防止 Visitor 在訪問相同節(jié)點(diǎn)時(shí)誤入到其中,但是它可能沒有需要的屬性,那么就非常容易出錯(cuò)或者誤傷,嚴(yán)格的控制節(jié)點(diǎn)的獲取流程將會(huì)省去不少不必要的麻煩。
需要注意什么State 狀態(tài)
狀態(tài)是抽象語法樹 AST 轉(zhuǎn)換的敵人,狀態(tài)管理會(huì)不斷牽扯我們的精力,而且?guī)缀跛心銓?duì)狀態(tài)的假設(shè),總是會(huì)有一些未考慮到的語法最終證明你的假設(shè)是錯(cuò)誤的。
Scope 作用域
在 JavaScript 中,每當(dāng)你創(chuàng)建了一個(gè)引用,不管是通過變量(variable)、函數(shù)(function)、類型(class)、參數(shù)(params)、模塊導(dǎo)入(import)還是標(biāo)簽(label)等,它都屬于當(dāng)前作用域。
當(dāng)編寫一個(gè)轉(zhuǎn)換時(shí),必須要小心作用域。我們得確保在改變代碼的各個(gè)部分時(shí)不會(huì)破壞已經(jīng)存在的代碼。在添加一個(gè)新的引用時(shí)需要確保新增加的引用名字和已有的所有引用不沖突,或者僅僅想找出使用一個(gè)變量的所有引用, 我們只想在給定的作用域(Scope)中找出這些引用。
作用域可以被表示為如下形式:
{ path: path, block: path.node, parentBlock: path.parent, parent: parentScope, bindings: [...] }
即在創(chuàng)建一個(gè)新的作用域的時(shí)候,需要給出它的路徑和父作用域,之后在遍歷的過程中它會(huì)在該作用域內(nèi)收集所有的引用,收集完畢后既可以在作用域上調(diào)用方法。
例如下面代碼中,我么需要將函數(shù)中的 n 轉(zhuǎn)換為 x 。
function square(n) { return n * n; } var n = 1; // 定義的 visitor(錯(cuò)誤版?) let paramName; const MyVisitor = { FunctionDeclaration(path) { const param = path.node.params[0]; paramName = param.name; param.name = "x"; }, Identifier(path) { if (path.node.name === paramName) { path.node.name = "x"; } } };
如果不考慮作用域的問題,則會(huì)導(dǎo)致函數(shù)外的 n 也被轉(zhuǎn)變,所以在轉(zhuǎn)換的過程中我們可以在 FunctionDeclaration 節(jié)點(diǎn)中進(jìn)行 n 的轉(zhuǎn)變,把需要遍歷的轉(zhuǎn)換方法放在其中,防止對(duì)外部的代碼產(chǎn)生作用。
// 改進(jìn)后 const updateParamNameVisitor = { Identifier(path) { if (path.node.name === this.paramName) { path.node.name = "x"; } } }; const MyVisitor = { FunctionDeclaration(path) { const param = path.node.params[0]; const paramName = param.name; param.name = "x"; path.traverse(updateParamNameVisitor, { paramName }); } }; path.traverse(MyVisitor);
Bindings 綁定
所有引用屬于特定的作用域,引用和作用域的這種關(guān)系稱作為綁定。
例如需要將 const 轉(zhuǎn)換為 var,并且對(duì) const 聲明的值給予只讀保護(hù)。
const a = 1; const b = 4; function test (){ let a = 2; a = 3; } a = 34;
而對(duì)于上面的這種情況,由于 function 有自己的作用域,所以在 function 內(nèi) a 可以被修改,而在外面則不能被修改。所以在實(shí)際應(yīng)用中就需要考慮到綁定關(guān)系。
使用配置常見做法是設(shè)置一個(gè)根目錄下的 .babelrc 文件,統(tǒng)一將 babel 的設(shè)置都放在這里。
常用 options 字段說明
env:env 的核心目的是通過配置得知目標(biāo)環(huán)境的特點(diǎn),然后只做必要的轉(zhuǎn)換。例如目標(biāo)瀏覽器支持 es2015,那么 es2015 這個(gè) preset 其實(shí)是不需要的,于是代碼就可以小一點(diǎn)(一般轉(zhuǎn)化后的代碼總是更長(zhǎng)),構(gòu)建時(shí)間也可以縮短一些。如果不寫任何配置項(xiàng),env 等價(jià)于 latest,也等價(jià)于 es2015 + es2016 + es2017 三個(gè)相加(不包含 stage-x 中的插件)。
plugins:要加載和使用的插件,插件名前的babel-plugin-可省略;plugin列表按從頭到尾的順序運(yùn)行
presets:要加載和使用的preset ,每個(gè) preset 表示一個(gè)預(yù)設(shè)插件列表,preset名前的babel-preset-可省略;presets列表的preset按從尾到頭的逆序運(yùn)行(為了兼容用戶使用習(xí)慣)
同時(shí)設(shè)置了presets和plugins,那么plugins的先運(yùn)行;每個(gè)preset和plugin都可以再配置自己的option
常見的配置方法
{ "plugins": [ "transform-remove-strict-mode", ["transform-nej-module", {"mode": "web"}] ], "presets": [ "env" ] }參考
Babel 插件手冊(cè)
Babel是如何讀懂JS代碼的
推薦工具
AST Explorer 在線生成 AST
Esprima 可以查看分詞結(jié)果
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/97687.html
摘要:整理收藏一些優(yōu)秀的文章及大佬博客留著慢慢學(xué)習(xí)原文協(xié)作規(guī)范中文技術(shù)文檔協(xié)作規(guī)范阮一峰編程風(fēng)格凹凸實(shí)驗(yàn)室前端代碼規(guī)范風(fēng)格指南這一次,徹底弄懂執(zhí)行機(jī)制一次弄懂徹底解決此類面試問題瀏覽器與的事件循環(huán)有何區(qū)別筆試題事件循環(huán)機(jī)制異步編程理解的異步 better-learning 整理收藏一些優(yōu)秀的文章及大佬博客留著慢慢學(xué)習(xí) 原文:https://www.ahwgs.cn/youxiuwenzhan...
摘要:的轉(zhuǎn)譯過程分為三個(gè)階段。標(biāo)準(zhǔn)為例,提供了如下的一些的只轉(zhuǎn)譯該年份批準(zhǔn)的標(biāo)準(zhǔn),而則代指最新的標(biāo)準(zhǔn),包括和。未完待續(xù),繼續(xù)學(xué)習(xí)繼續(xù)補(bǔ)充哦參考文獻(xiàn)深入理解原理及其使用 Babel Babel的轉(zhuǎn)譯過程分為三個(gè)階段: parsing, transforming, generating。babel只是轉(zhuǎn)譯新標(biāo)準(zhǔn)引入的語法,比如ES6的箭頭函數(shù)轉(zhuǎn)譯成ES5的函數(shù);而新標(biāo)準(zhǔn)引入的原生對(duì)象,部分原生對(duì)...
摘要:下面是用實(shí)現(xiàn)轉(zhuǎn)成抽象語法樹如下還支持繼承以下是轉(zhuǎn)換結(jié)果最終的結(jié)果還是代碼,其中包含庫中的一些函數(shù)。可以使用新的易于使用的類定義,但是它仍然會(huì)創(chuàng)建構(gòu)造函數(shù)和分配原型。 這是專門探索 JavaScript 及其所構(gòu)建的組件的系列文章的第 15 篇。 想閱讀更多優(yōu)質(zhì)文章請(qǐng)猛戳GitHub博客,一年百來篇優(yōu)質(zhì)文章等著你! 如果你錯(cuò)過了前面的章節(jié),可以在這里找到它們: JavaScript 是...
摘要:深入淺出指的是添加在標(biāo)準(zhǔn)第六版中的編程語言的新特性,簡(jiǎn)稱為。登場(chǎng)在一個(gè)具體的項(xiàng)目中,使用有幾種不同的方法。這是為了避免在安裝時(shí)使用根管理權(quán)限。以用戶角度展示系統(tǒng)響應(yīng)速度,以地域和瀏覽器維度統(tǒng)計(jì)用戶使用情況。 深入淺出 ES6 指的是添加在 ECMASript 標(biāo)準(zhǔn)第六版中的 JavaScript 編程語言的新特性,簡(jiǎn)稱為 ES6。 雖然 ES6 剛剛到來,但是人們已經(jīng)開始談?wù)?ES7 ...
摘要:接著上一篇文章深入了解一的處理步驟的三個(gè)主要處理步驟分別是解析,轉(zhuǎn)換,生成。模塊是的代碼生成器,它讀取并將其轉(zhuǎn)換為代碼和源碼映射抽象語法樹抽象語法樹在以上三個(gè)神器中都出現(xiàn)過,所以對(duì)于編譯器來說至關(guān)重要。 接著上一篇文章《深入了解babel(一)》 Babel 的處理步驟 Babel 的三個(gè)主要處理步驟分別是: 解析(parse),轉(zhuǎn)換(transform),生成(generate)。對(duì)...
閱讀 816·2023-04-25 20:18
閱讀 2097·2021-11-22 13:54
閱讀 2537·2021-09-26 09:55
閱讀 3903·2021-09-22 15:28
閱讀 2978·2021-09-03 10:34
閱讀 1714·2021-07-28 00:15
閱讀 1637·2019-08-30 14:25
閱讀 1285·2019-08-29 17:16