[babel] babel的工作原理
Babel是什么
Babel 是一個通用的多功能的 JavaScript 編譯器。主要用于將采用 ECMAScript 2015+ 語法編寫的代碼轉換為向后兼容的 JavaScript 語法,以便能夠運行在當前和舊版本的瀏覽器或其他環境中。
常見的用途有:
- 語法轉換
- 通過 Polyfill 方式在目標環境中添加缺失的功能(通過引入第三方 polyfill 模塊,例如 core-js)
- 源碼轉換(codemods)
例如我們在React經常使用 JSX 語法,由于這不是JS原生語法,所以不能直接被 JS 引擎編譯執行。在代碼被執行之前,需要使用編譯器進行轉譯,轉換成 JS 代碼。
Babel的工作原理
從源代碼轉換到目標代碼的過程,也就是從源字符串轉換到目標字符串的過程。當然我們不可能直接在字符串上進行操作。在 Babel 轉換代碼的中間過程會生成抽象語法樹(Abstract Syntax Tree,簡稱AST),用樹來表示代碼的結構和語義,然后通過遍歷這棵樹,并在節點上應用一些操作來完成對代碼的轉換操作。
Babel的主要處理步驟是:解析(parse)、轉換(transform)、生成(generate)。
對于不了解編譯原理的前端開發人員,這里推薦一個
github上面的mini級別的項目:jamiebuilds/the-super-tiny-compiler: ? Possibly the smallest compiler ever (github.com)這是一個使用
JS編寫的超級簡單但是包含了上述三個主要步驟的編譯器。代碼就幾百行,加上注釋有一千多行,講解非常詳細。適合入門。
解析 parse
“解析”這一過程主要是通過讀取代碼字符串,構建出 AST 。
這一過程主要包含詞法分析和語法分析這兩個階段。
詞法分析
詞法分析部分使用tokenizer方法記錄一個tokens列表,為代碼中的每一個token標注其類型和值:
從源碼中可以看到Token除了記錄類型和值,還記錄了這個詞在源代碼中的位置,即start、end、loc等屬性。
token是指代碼中獨立的最小單元,它可以是數字字面量(
NumberLiteral)、字符串字面量(StringLiteral)、操作符(operator)等等。
export class Token {
constructor(state: State) {
this.type = state.type;
this.value = state.value;
this.start = state.start;
this.end = state.end;
this.loc = new SourceLocation(state.startLoc, state.endLoc);
}
declare type: TokenType;
declare value: any;
declare start: number;
declare end: number;
declare loc: SourceLocation;
}
在詞法分析完成之后,會得到一個tokens數組,記錄了代碼中每個詞的記錄。
比如下面這個簡單的語句:
n * n;
詞法分析之后將得到如下的數組:
[
{ type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
{ type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
{ type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
...
]
type是一個對象,通過一些屬性來描述一個token:
{
type: {
label: 'name',
keyword: undefined,
beforeExpr: false,
startsExpr: true,
rightAssociative: false,
isLoop: false,
isAssign: false,
prefix: false,
postfix: false,
binop: null,
updateContext: null
},
...
}
語法分析
這一階段會根據上一階段生成的tokens構造出AST的表述結構。
相關聯的tokens會被組合成語句,形成子樹,即子樹的根節點是描述表達式的節點,子節點是token產生的節點或者嵌套描述其它表達式的節點。
AST的Node并不是直接復用上述Token的數據結構,而是一個新的數據結構。
這里用AST explorer進行舉例。
語句:n*n;
生成的AST如下:
{
"type": "Program",
"start": 0,
"end": 6,
"body": [
{
"type": "ExpressionStatement",
"start": 0,
"end": 6,
"expression": {
"type": "BinaryExpression",
"start": 0,
"end": 5,
"left": {
"type": "Identifier",
"start": 0,
"end": 1,
"name": "n"
},
"operator": "*",
"right": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "n"
}
}
}
],
"sourceType": "module"
}
語法分析完成之后就得到了抽象語法樹。
轉換 transform
轉換操作通過遍歷抽象語法樹,對節點進行新增、更新、刪除等操作。這是Babel工作流程中最復雜的部分,也是babel插件介入工作的主要部分。
Visitor
Babel使用深度優先遍歷 AST,這個過程中使用了訪問者模式。即構建一個visitor對象,遍歷過程中針對節點的類型,執行不同的方法,而方法又細分為enter和exit兩個與時間相關的hook。
visitor示例:
const MyVisitor = {
Identifier: {
enter() {
...
},
exit() {
...
}
},
CallExpression: {
enter(){
...
},
exit(){
...
}
},
...
};
NodePath
在遍歷 AST 的時候,babel還會生成NodePath對象,這個對象包含了節點本身以及與節點相關的上下文信息,比如父節點、兄弟節點和作用域信息等。NodePath對象的數據結構大致如下(不止這些屬性):(摘自官方handbook)
{
// 當前節點的父節點,表示這是一個函數聲明(FunctionDeclaration)
"parent": {
"type": "FunctionDeclaration",
"id": {...}, // 函數聲明的標識符節點(具體內容省略)
....
},
// 當前路徑對應的 AST 節點,這是一個標識符(Identifier),其名稱是 "square"
"node": {
"type": "Identifier",
"name": "square"
},
// 包含處理工具的對象,通常包括 `file` 屬性,用于訪問文件信息和其他上下文信息
"hub": {...},
// 保存路徑上下文的棧,用于處理嵌套的路徑操作
"contexts": [],
// 存儲與路徑相關的自定義數據
"data": {},
// 標記是否應跳過當前路徑的遍歷
"shouldSkip": false,
// 標記是否應停止整個遍歷過程
"shouldStop": false,
// 標記當前節點是否已被刪除
"removed": false,
// 在遍歷過程中存儲插件的狀態信息
"state": null,
// 當前路徑的選項對象,通常用于配置遍歷選項
"opts": null,
// 指示是否應跳過某些子節點
"skipKeys": null,
// 當前節點父節點的 `NodePath` 對象
"parentPath": null,
// 當前路徑的上下文信息
"context": null,
// 當前節點所在的容器(可能是父節點的屬性或數組)
"container": null,
// 如果當前節點在父節點中是一個列表的一部分,則為列表的鍵
"listKey": null,
// 布爾值,指示當前節點是否在其父節點的列表中
"inList": false,
// 當前節點在其父節點中的鍵
"parentKey": null,
// 當前節點在父節點中的位置
"key": null,
// 當前路徑的作用域(scope)對象
"scope": null,
// 當前路徑節點的類型(在某些上下文中使用)
"type": null,
// 當前路徑節點的類型注解(TypeScript 或 Flow)
"typeAnnotation": null,
......
}
自定義插件簡單示例:
my-plugin.js
module.exports = function (babel) {
const { types: t } = babel;
return {
visitor: {
Identifier(path) {
// `path` 是一個 `NodePath` 對象,代表當前的標識符節點
if (path.node.name === 'oldName') {
// 修改當前標識符節點的名稱
path.replaceWith(t.identifier('newName'));
}
},
},
};
};
Identifier如果配置為一個函數,則默認是enter執行;- 傳入的參數
path就是NodePath對象,包含當前節點、上下文、作用域等信息,以及一些更新節點的方法; - 相關的方法見官方文檔:babel-handbook/translations/en/plugin-handbook.md at master · jamiebuilds/babel-handbook (github.com)
- 新增兄弟節點:
path.insertBefore,path.insertAfter; - 刪除節點:
path.remove - 更新節點:
path.replaceWith
- 新增兄弟節點:
在babel配置文件中(例如.babelrc):使用文件路徑配置目標插件
{
"presets": ["@babel/preset-env"],
"plugins": ["./my-plugin"]
}
生成 generate
babel通過babel-generator模塊深度優先遍歷 AST ,根據節點類型生成相應的代碼片段,并根據配置選項生成不同格式的代碼和源碼映射。
這個階段的產物是:編譯后的代碼 + source-map。
結語
babel的優點在于為JS提供了許多可能性。
對于web前端開發人員來說,他們不再需要過度糾結語法的兼容性,babel會完成代碼降級兼容舊版本;
對于babel插件開發人員來說,babel是一個便捷地操作抽象語法樹的工具,我們不再需要手寫編譯器,起碼不需要實現parse和generate,只需要將注意力集中在最核心的visitor,關注如何對 AST 進行 transform。
參考資料
[2] EmberConf 2016: How to Build a Compiler by James Kyle - YouTube
[3] jamiebuilds/the-super-tiny-compiler: ? Possibly the smallest compiler ever (github.com)
[4] https://www.babeljs.cn/docs/
[5] AST explorer

浙公網安備 33010602011771號