Babel 是怎么工作的
Babel
是一個 JavaScript
編譯器。
做與不做
注意很重要的一點就是,Babel
只是轉譯新標准引入的語法,比如:
- 箭頭函數
- let / const
- 解構
哪些在 Babel 范圍外?對於新標准引入的全局變量、部分原生對象新增的原型鏈上的方法,Babel 表示超綱了。
- 全局變量
- Promise
- Symbol
- WeakMap
- Set
- includes
- generator 函數
對於上面的這些 API,Babel
是不會轉譯的,需要引入 polyfill
來解決。
Babel 編譯的三個階段
Babel 的編譯過程和大多數其他語言的編譯器相似,可以分為三個階段:
- 解析(Parsing):將代碼字符串解析成抽象語法樹。
- 轉換(Transformation):對抽象語法樹進行轉換操作。
- 生成(Code Generation): 根據變換后的抽象語法樹再生成代碼字符串。
為了理解 Babel
,我們從最簡單一句 console
命令下手
解析(Parsing)
Babel
拿到源代碼會把代碼抽象出來,變成 AST
(抽象語法樹),學過編譯原理的同學應該都聽過這個詞,全稱是 Abstract Syntax Tree。
抽象語法樹是源代碼的抽象語法結構的樹狀表示,樹上的每個節點都表示源代碼中的一種結構,只所以說是抽象的,是因為抽象語法樹並不會表示出真實語法出現的每一個細節,
比如說,嵌套括號被隱含在樹的結構中,並沒有以節點的形式呈現,它們主要用於源代碼的簡單轉換。
console.log('zcy');
的 AST 長這樣:
{
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"computed": false,
"object": {
"type": "Identifier",
"name": "console"
},
"property": {
"type": "Identifier",
"name": "log"
}
},
"arguments": [
{
"type": "Literal",
"value": "zcy",
"raw": "'zcy'"
}
]
}
}
],
"sourceType": "script"
}
上面的 AST
描述了源代碼的每個部分以及它們之間的關系,可以自己在這里試一下 astexplorer。
AST 是怎么來的?
整個解析過程分為兩個步驟:
- 分詞:將整個代碼字符串分割成語法單元數組 在線分詞工具
語法單元通俗點說就是代碼中的最小單元,不能再被分割,就像原子是化學變化中的最小粒子一樣。Javascript
代碼中的語法單元主要包括以下這么幾種:
- 關鍵字:
const
、let
、var
等 - 標識符:可能是一個變量,也可能是 if、else 這些關鍵字,又或者是 true、false 這些常量
- 運算符
- 數字
- 空格
- 注釋:對於計算機來說,知道是這段代碼是注釋就行了,不關心其具體內容
其實分詞說白了就是簡單粗暴地對字符串一個個遍歷。為了模擬分詞的過程,寫了一個簡單的 Demo,僅僅適用於和上面一樣的簡單代碼。Babel 的實現比這要復雜得多,但是思路大體上是相同的。
- 語法分析:建立分析語法單元之間的關系
語義分析是將得到的詞匯進行一個立體的組合,確定詞語之間的關系。考慮到編程語言的各種從屬關系的復雜性,語義分析的過程又是在遍歷得到的語法單元組,相對而言就會變得更復雜。
簡單來說語法分析是對語句和表達式識別,這是個遞歸過程,在解析中,Babel
會在解析每個語句和表達式的過程中設置一個暫存器,用來暫存當前讀取到的語法單元,如果解析失敗,就會返回之前的暫存點,再按照另一種方式進行解析,
如果解析成功,則將暫存點銷毀,不斷重復以上操作,直到最后生成對應的語法樹。
轉換(Transformation)
Plugins
插件應用於babel
的轉譯過程,尤其是第二個階段Transformation
,如果這個階段不使用任何插件,那么babel
會原樣輸出代碼。
Presets
plugins是一個小型的js代碼程序,告訴Babel如何轉換你的源碼,比如 @babel/plugin-transform-arrow-functions
的作用就是將es2015的箭頭函數轉換成普通函數:
有那么多新的語法, 我們總不能一個一個的引入吧,於是就產生了預設: Presets,
官方幫我們做了一些預設的插件集,稱之為
顧名思義——預設,它包含了一組我們需要的plugins BabelPreset
,
這樣我們只需要使用對應的 Preset 就可以了, 而 babel-preset-env
相當於 ES2015 ,ES2016 ,ES2017 及最新版本。
Polyfill
中文翻譯是墊片,之前沒有詳細了解babel之前,我也很迷茫這個polyfill是啥,因為語法不都給你轉換好了,還需要這個東西干啥,后來仔細想了一下,要適應新特性應該從兩方面入手:
-
語法轉換:
() => {};
for (let i of items) {};
比如箭頭函數、for...of,在不支持這些語法的環境下,直接會報語法錯誤,因為編譯器根本不知道 =>
這些是什么鬼符號,要做到讓編譯器識別,那就要將這樣的語法轉換成瀏覽器能識別的代碼,那么就需要語法轉換。
2.功能補充
比如 'foo'.includes('f'),
es2015里不僅只有新的語法,還有實例的擴展,比如String,其實這里只是調用了String實例的一個方法,我們無論怎么語法轉換也沒有什么用吧,如果我們在不支持String.prototype.includes的編譯器里跑這些代碼,會得到 'foo'.includes is not a function. 這樣的一個報錯,而不是語法報錯。
Polyfill提供的就是一個這樣功能的補充,實現了Array、Object等上的新方法,實現了Promise、Symbol這樣的新Class等。
雖然@babel/polyfill提供了我們想要的所有新方法新類,但是這里依然存在一些問題:
- 體積太大:比如我只用了String的新特性,但是我把整個包都引進來了,這不是徒增了很多無用的代碼。
- 污染全局環境:如果你引用了
@babel/polyfill
,那么像Promise這樣的新類就是掛載在全局上的,這樣就會污染了全局命名空間。可能在一個團建建立的項目問題不太大,但是如果你是一個工具的開發者,你把全局環境污染了,別人用你的工具,就有可能把別人給坑了。
一個解決方案就是引入transform runtime 來替代 @babel/polyfill,像下面這樣的配置:
{
"plugins": [
["transform-runtime", {
"helpers": false, //自動引入helpers
"polyfill": false, //自動引入polyfill(core-js提供的polyfill)
"regenerator": true, //自動引入regenerator
}]
]
}
另一個解決方案就是 @babel/preset-env 這個preset,它有一個useBuiltIns選項,如果設置成"usage"
,那么將會自動檢測語法幫你require你代碼中使用到的功能。
const presets = [
[
"@babel/env",
{
useBuiltIns: "usage",
},
],
];
比如我在代碼中:
Promise.resolve().finally();
如果在edge17不支持這個特性的環境里運行,將會幫你編譯成:
require("core-js/modules/es.promise.finally");
Promise.resolve().finally();
比較 transform-runtime 與 babel-polyfill 引入墊片的差異:
1.使用runtime是按需引入,需要用到哪些polyfill,runtime就自動幫你引入哪些,不需要再手動一個個的去配置plugins,只是引入的polyfill不是全局性的,有些局限性。而且runtime引入的polyfill不會改寫一些實例方法,比如Object和Array原型鏈上的方法,像前面提到的Array.protype.includes。
2.babel-polyfill就能解決runtime的那些問題,它的墊片是全局的,而且全能,基本上ES6中要用到的polyfill在babel-polyfill中都有,它提供了一個完整的ES6+的環境。babel官方建議只要不在意babel-polyfill的體積,最好進行全局引入,因為這是最穩妥的方式。
3.一般的建議是開發一些框架或者庫的時候使用不會污染全局作用域的babel-runtime,而開發web應用的時候可以全局引入babel-polyfill避免一些不必要的錯誤,而且大型web應用中全局引入babel-polyfill可能還會減少你打包后的文件體積(相比起各個模塊引入重復的polyfill來說)。
Plugin/Preset 路徑
如果 Plugin 是通過 npm 安裝,可以傳入 Plugin 名字給 Babel,Babel 將檢查它是否安裝在 node_modules
中
"plugins": ["babel-plugin-myPlugin"]
也可以指定你的 Plugin/Preset 的相對或絕對路徑。
"plugins": ["./node_modules/asdf/plugin"]
Plugin/Preset 排序
如果兩次轉譯都訪問相同的節點,則轉譯將按照 Plugin 或 Preset 的規則進行排序然后執行。
- Plugin 會運行在 Preset 之前。
- Plugin 會從第一個開始順序執行。
- Preset 的順序則剛好相反(從最后一個逆序執行)。
例如:
{
"plugins": [
"transform-decorators-legacy",
"transform-class-properties"
]
}
將先執行transform-decorators-legacy
再執行transform-class-properties
但 preset 是反向的
{
"presets": [
"es2015",
"react",
"stage-2"
]
}
會按以下順序運行:stage-2
,react
, 最后es2015
。
那么問題來了,如果presets
和plugins
同時存在,那執行順序又是怎樣的呢?答案是先執行plugins
的配置,再執行presets
的配置。所以以下代碼的執行順序為
- @babel/plugin-proposal-decorators
- @babel/plugin-proposal-class-properties
- @babel/plugin-transform-runtime
- @babel/preset-env
// .babelrc 文件
{
"presets": [
[
"@babel/preset-env"
]
],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }],
"@babel/plugin-transform-runtime",
]
}
生成(Code Generation)
用 babel-generator
通過 AST 樹生成 ES5 代碼
演示代碼地址:點我
參考 :
前端工程師必須掌握的Babel知識 , Babel 7.1介紹 , Babel教程 , Babel該如何配置