簡介
Babel是通用的多用途的javascript編譯器。不僅如此,它還是一組用于多種不同形態(tài)的靜態(tài)分析模塊。
靜態(tài)分析 是指在不執(zhí)行代碼的前提下分析代碼。(與之相對應(yīng)的就是動態(tài)分析,代碼執(zhí)行過程中分析代碼)。靜態(tài)分析的目的是多樣性的,比如可以用于
代碼校驗/編譯/高亮/代碼轉(zhuǎn)換/優(yōu)化/壓縮等等。
讀者可以使用Babel開發(fā)多種不同的工具以幫助提升效率和編寫更好的程序。
基礎(chǔ)
Babel是一種javascript編譯器,確切地說是轉(zhuǎn)換器,即源碼到源碼的編譯器。通俗地說,Babel接收一段javascript代碼并修改它,然后輸出新的符合開發(fā)者預(yù)期的代碼。
ASTs (抽象語法樹)
下列的步驟都會涉及到創(chuàng)建&操作AST。
function square(n) {
return n * n;
}
點擊查看上述代碼對應(yīng)的AST,有助于理解AST節(jié)點
代碼轉(zhuǎn)成的AST信息如下:
- FunctionDeclaration:
- id:
- Identifier:
- name: square
- params [1]
- Identifier
- name: n
- body:
- BlockStatement
- body [1]
- ReturnStatement
- argument
- BinaryExpression
- operator: *
- left
- Identifier
- name: n
- right
- Identifier
- name: n
相應(yīng)的json格式為:
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
params: [{
type: "Identifier",
name: "n"
}],
body: {
type: "BlockStatement",
body: [{
type: "ReturnStatement",
argument: {
type: "BinaryExpression",
operator: "*",
left: {
type: "Identifier",
name: "n"
},
right: {
type: "Identifier",
name: "n"
}
}
}]
}
}
細心的讀者應(yīng)該已經(jīng)發(fā)現(xiàn),AST中不同類型的節(jié)點,結(jié)構(gòu)是相似的
- 函數(shù)定義
{
type: "FunctionDeclaration",
id: {...},
params: [...],
body: {...}
}
- 變量
{
type: "Identifier",
name: ...
}
- 表達式
{
type: "BinaryExpression",
operator: ...,
left: {...},
right: {...}
}
上述的結(jié)構(gòu)并不是對應(yīng)節(jié)點的全部屬性,主要作為說明,選取部分屬性
AST由一個單獨的Node,或者成百上千個Node構(gòu)成。這些節(jié)點表達了程序的語法信息,基于此可以做程序的靜態(tài)分析。
每一個Node都從基礎(chǔ)的接口派生而出
interface Node {
type: string;
}
字段type表示節(jié)點的類型(如 FunctionDeclaration,Identifier 等)。不同類型的節(jié)點會定義額外的字段來詳細描述自身。
Babel也會在節(jié)點上添加一些節(jié)點在源碼中的位置信息的屬性,比如下面這種:
{
type: ...,
start: 0,
end: 38,
loc: {
start: {
line: 1,
column: 0
},
end: {
line: 3,
column: 1
}
},
...
}
start,end,loc存在于所有的節(jié)點中。
Babel處理的階段
Babel有3個重要的階段parse,transform,generate。
Parse
詞法分析
詞法分析的過程是將代碼字符串轉(zhuǎn)換為一串 tokens。
以下面的代碼語句為例,tokens可以理解為語法片段的平鋪。
n * n
對應(yīng)為
[
{ type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
{ type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
{ type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
...
]
每一個type包含一系列的屬性,如
{
type: {
label: 'name',
keyword: undefined,
beforeExpr: false,
startsExpr: true,
rightAssociative: false,
isLoop: false,
isAssign: false,
prefix: false,
postfix: false,
binop: null,
updateContext: null
},
...
}
token中也包含start,end 和 loc信息,這一點跟AST的節(jié)點一樣。
語法分析
語法分析的過程就是把上述的tokens轉(zhuǎn)換成AST?;?code>tokens中的信息,對tokens進行樹形構(gòu)建,使其對使用者更為友好,構(gòu)建產(chǎn)物的結(jié)構(gòu)是樹形結(jié)構(gòu),代表語法片段間的關(guān)系,也就是我們通常說的AST。
Transform
transform階段是對給定的AST進行遍歷,基于特定的目的對AST中的節(jié)點進行添加/修改/刪除。這個部分是Babel或其他編譯器最為復(fù)雜的。這個階段也是Babel插件介入的階段,插件的部分在相關(guān)章節(jié)會詳細闡述,這里就不再贅述。
Generate
代碼生成階段基于最后生成的AST輸出代碼字符串和source maps。生成代碼相對簡單:深度優(yōu)先遍歷AST,構(gòu)建代表相應(yīng)語義的代碼字符串。
Traversal
如果要對AST進行transform,那么就必須遞歸遍歷AST的節(jié)點。
以前文的函數(shù)定義節(jié)點FunctionDeclaration為例,它包含一些屬性:id,params和body。而這些屬性也都包含嵌套的節(jié)點。
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
params: [{
type: "Identifier",
name: "n"
}],
body: {
type: "BlockStatement",
body: [{
type: "ReturnStatement",
argument: {
type: "BinaryExpression",
operator: "*",
left: {
type: "Identifier",
name: "n"
},
right: {
type: "Identifier",
name: "n"
}
}
}]
}
}
因此,遍歷上述AST的順序如下
- 從
FunctionDeclaration節(jié)點開始,遍歷其屬性列表 - 訪問
id屬性,類型為Identifier,它沒有子節(jié)點,繼續(xù)訪問 - 訪問
params屬性,類型為節(jié)點數(shù)組,因此遍歷該數(shù)組,每個節(jié)點都是Identifier,繼續(xù)訪問 - 訪問
body屬性,類型為BlockStatement,這個類型有body屬性,為一組節(jié)點,遍歷該節(jié)點數(shù)組 -
BlockStatement只有1個ReturnStatement的節(jié)點,該節(jié)點有argument屬性 - 訪問
argument,其類型為BinaryExpression,包含operator,left,right屬性。 -
operator不是節(jié)點,繼續(xù)訪問 - 訪問
left和right對應(yīng)的節(jié)點
節(jié)點訪問貫穿于整個transform階段。
Visitors
訪問節(jié)點的概念源于visitor設(shè)計模式。
Visitors是一種跨語言遍歷AST的模式。簡單地說,Visitors是一個對象,其中定義了用于接受AST中特定節(jié)點類型的方法。這么說顯得比較抽象,可以看下面的示意代碼。
// 方法直接定義在對象上
const MyVisitor = {
Identifier() {
console.log("Called!");
}
};
// 先創(chuàng)建對象,再附加屬性
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}
注意: Identifier() { ... } 是對 Identifier: { enter() { ... } }的簡寫。
MyVisitor定義了一個在遍歷(traversal)過程中的基礎(chǔ)訪問器,訪問的每個類型為Identifier的節(jié)點時都會調(diào)用該訪問器。對于下面的示例代碼
function square(n) {
return n * n;
}
因為包含4個標識符節(jié)點(square,n,n,n),因此Identifier訪問器方法會被調(diào)用4次。
path.traverse(MyVisitor);
// Called!
// Called!
// Called!
// Called!
這些方法調(diào)用都發(fā)生在enter節(jié)點時,也可以在exit節(jié)點時調(diào)用。
假設(shè)有如下的語法樹結(jié)構(gòu)
- FunctionDeclaration
- Identifier (id)
- Identifier (params[0])
- BlockStatement (body)
- ReturnStatement (body)
- BinaryExpression (argument)
- Identifier (left)
- Identifier (right)
整個的遍歷順序(包含enter和exit)的示意如下
-
Enter FunctionDeclaration
-
Enter Identifier (id)
- Hit dead end
- Exit Identifier (id)
-
Enter Identifier (params[0])
- Hit dead end
- Exit Identifier (params[0])
-
Enter BlockStatement (body)
-
Enter ReturnStatement (body)
-
Enter BinaryExpression (argument)
-
Enter Identifier (left)
- Hit dead end
- Exit Identifier (left)
-
Enter Identifier (right)
- Hit dead end
- Exit Identifier (right)
-
Enter Identifier (left)
- Exit BinaryExpression (argument)
-
Enter BinaryExpression (argument)
- Exit ReturnStatement (body)
-
Enter ReturnStatement (body)
- Exit BlockStatement (body)
-
Enter Identifier (id)
- Exit FunctionDeclaration
因此,訪問器有2次機會訪問1個節(jié)點,即在enter和exit的時候。
const MyVisitor = {
Identifier: {
enter() {
console.log("Entered!");
},
exit() {
console.log("Exited!");
}
}
};
如果想讓多個訪問器使用同一個方法訪問節(jié)點,可以使用|將不同的訪問器隔開,形如Identifier|MemberExpression。
flow-comments中的使用示例如下:
"ExportNamedDeclaration|Flow"(path) {
let { node, parent } = path;
if (t.isExportNamedDeclaration(node) && !t.isFlow(node.declaration)) {
return;
}
wrapInFlowComment(path, parent);
},
訪問器也可以使用在babel-types中定義的節(jié)點別名。
例如:
Function是FunctionDeclaration,FunctionExpression,ArrowFunctionExpression,ObjectMethod和ClassMethod的別名。
const MyVisitor = {
Function(path) {}
//等價于
//'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression|ObjectMethod|ClassMethod'(path) {}
};
Paths
一個 AST通常有很多節(jié)點,但是節(jié)點之間是如何相互關(guān)聯(lián)的呢?通過一個完全可控的巨大可變對象,對該對象進行操作,同時使用 Paths 來簡化對象的操作。
一個Path代表兩個節(jié)點的關(guān)聯(lián)信息。以下面的結(jié)構(gòu)為例:
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
...
}
Identifier作為path所包含的信息如下:
{
// 記錄父節(jié)點的信息,可以通過 parent 快速訪問到 FunctionDeclaration
"parent": {
"type": "FunctionDeclaration",
"id": {...},
....
},
"node": {
"type": "Identifier",
"name": "square"
}
}
上面只是一個簡單的示意,完整的path還包含其他信息
{
"parent": {...},
"node": {...},
"hub": {...},
"contexts": [],
"data": {},
"shouldSkip": false,
"shouldStop": false,
"removed": false,
"state": null,
"opts": null,
"skipKeys": null,
"parentPath": null,
"context": null,
"container": null,
"listKey": null,
"inList": false,
"parentKey": null,
"key": null,
"scope": null,
"type": null,
"typeAnnotation": null
}
還有其他涉及到節(jié)點添加/更新/移動/刪除的各種方法,這里就不贅述,在后續(xù)相關(guān)章節(jié)討論。
某種意義上講,paths是包含節(jié)點在AST上的位置以及節(jié)點各種信息的響應(yīng)式對象。更新AST的同時,這些相關(guān)的信息也會同步更新。Babel將這些同步更新對開發(fā)者透明,使得操作節(jié)點盡量簡單。
Paths in Visitors
在訪問器的方法中,實際訪問的是path,而非node本身。比如:
- 定義訪問器
const MyVisitor = {
Identifier(path) {
console.log("Visiting: " + path.node.name);
}
};
- 待遍歷的源碼
a + b + c
- 遍歷
path.traverse(MyVisitor);
// Visiting: a
// Visiting: b
// Visiting: c
在對AST進行transform時,要特別關(guān)注State,如果處理不當,會導(dǎo)致實際處理的結(jié)果跟預(yù)期不一致。
以下面的代碼為例
function square(n) {
return n * n;
}
現(xiàn)在有個轉(zhuǎn)換需求,將變量n變成x,使用前述的訪問器來處理。
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";
}
}
};
使用函數(shù)定義訪問器和變量訪問器,修改函數(shù)的參數(shù)名和函數(shù)體中的變量名。
但是如果代碼變成下面這樣呢?
function square(n) {
return n * n;
}
const n = 3;
如果使用上面的訪問器,就會出問題。因為在square下面有另外一個變量名為n的變量。但我們的需求是修改函數(shù)中的變量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";
// 在訪問器中,向下遞歸訪問子節(jié)點
path.traverse(updateParamNameVisitor, { paramName });
}
};
path.traverse(MyVisitor);
上面的示例演示了全局狀態(tài)對于訪問器的影響,同時也給出了相應(yīng)的消除外部狀態(tài)影響的方案。
Scopes
javascript有詞法作用域。詞法作用域是一個由不同的塊創(chuàng)建而成的樹形結(jié)構(gòu)。
// global scope
function scopeOne() {
// scope 1
function scopeTwo() {
// scope 2
}
}
在javascript中創(chuàng)建的引用(可以是 變量/函數(shù)/類/變量 等)屬于當前作用域。
var global = "I am in the global scope";
function scopeOne() {
var one = "I am in the scope created by `scopeOne()`";
function scopeTwo() {
var two = "I am in the scope created by `scopeTwo()`";
}
}
代碼中使用當前作用域外層的變量引用
function scopeOne() {
// scopeOne下的變量
var one = "I am in the scope created by `scopeOne()`";
function scopeTwo() {
// 引用外部的變量也是可以的,會修改外部引用的值
one = "I am updating the reference in `scopeOne` inside `scopeTwo`";
}
}
代碼中定義與外層變量同名的變量
function scopeOne() {
var one = "I am in the scope created by `scopeOne()`";
function scopeTwo() {
// 定義同名的變量,這里的修改不會修改到外部的同名引用
var one = "I am creating a new `one` but leaving reference in `scopeOne()` alone.";
}
}
transform時要警惕變量的作用域,避免修改代碼時導(dǎo)致破壞其他部分的代碼。換言之,對變量的處理需要在它所屬的作用域內(nèi)。
作用的結(jié)構(gòu)如下所示
{
path: path,
block: path.node,
parentBlock: path.parent,
parent: parentScope,
bindings: [...]
}
創(chuàng)建一個新的作用域時,會賦予其對應(yīng)的path和關(guān)聯(lián)父作用域。遍歷時收集其作用域內(nèi)所有的引用(bindings)。
Bindings
引用都是在特定的作用域內(nèi)的,引用跟作用域之間的關(guān)系叫做binding,簡單理解就是把引用綁定到作用域上。
function scopeOnce() {
var ref = "This is a binding";
ref; // This is a reference to a binding
function scopeTwo() {
ref; // This is a reference to a binding from a lower scope
}
}
binding的結(jié)構(gòu)如下
{
identifier: node,
scope: scope,
path: path,
kind: 'var',
referenced: true,
references: 3,
referencePaths: [path, path, path],
constant: false,
constantViolations: [path]
}
通過上面的binding信息,可以獲取很多信息
- binding的類型(
參數(shù)/變量等) - 所有對該binding的引用
- 所屬的作用域
- 判斷變量是否為常量(
引用是否在某個地方被修改) - 修改變量的path列表(
哪些地方修改了引用值) - ...
判斷binding是否constant在很多場景下非常有用,比如在代碼壓縮的應(yīng)用中影響巨大(修改引用的名稱時,需要將引用到它的地方變量名一起修改)。
function scopeOne() {
// 創(chuàng)建binding,無代碼對其引用修改,固constant為true
var ref1 = "This is a constant binding";
becauseNothingEverChangesTheValueOf(ref1);
function scopeTwo() {
// 創(chuàng)建binding
var ref2 = "This is *not* a constant binding";
// 修改引用ref2,導(dǎo)致binding的constant為false
ref2 = "Because this changes the value";
}
}
至此,初步翻譯Babel文檔中相關(guān)的基本概念和示例代碼理解,如果錯誤歡迎交流指正。