webpack 的 scope hositing 實現原理,它也是一種 tree shaking!

scope hoisting 是 webpack 3 就已經實現的功能了,它能優化生成代碼的性能,還能實現部分 tree shaking。

那什麼是 scope hoisting,它又是怎麼實現的呢?

我們一起來看一下:

這樣一個 example.js 的入口模塊,它引用了 a 模塊:

a 模塊導出了一個 a 變量,又引入了 x 模塊:

x 模塊導出了 x 變量,引入了 y 模塊:

y 模塊導出了 y 變量:

就是一個模塊的引用鏈條,比較容易搞懂:

有這樣一個 webpack 配置文件:

開發模式,不生成 sourcemap,入口是 example.js。

生成的代碼是這樣的:

a、x、y 都被一個函數包裹:

然後在入口模塊引入:

這個很正常,因爲瀏覽器裏實現模塊就是用函數的方式嘛。

這是沒有開啓 scope hositing 的時候。

我們接下來開啓 scope hositing 看看會發生什麼。

開啓的話加一個 optimization.concatenateModules 爲 true 的配置就好了:

concatenateModules 是連接模塊的意思。

重新執行 webpack,這時候生成的代碼是這樣的:

a、x、y 模塊的代碼被合併到了一個函數作用域裏。

這就是 scope hoisting 的功能。

有的同學說,這樣合併有問題的吧,如果有同名變量怎麼辦?

我們試一下:

在 a 模塊定義了一個 aa 變量:

在 x 模塊也定義了一個 aa 變量:

重新跑下 webpack,生成的代碼是這樣的:

可以看到同名的變量被加上文件名的前綴(上面一張圖重複了忘刪了):

這樣同名變量經過重命名後,就不再會衝突了。

那 scope hoisting 有什麼好處呢?

很容易想到的是性能的提升,本來要創建好幾個函數的閉包,因爲被別的函數作用域引用嘛,現在只需要創建一個了,佔據的內存會更小。

再就天然能實現 tree shaking。

比如 x 模塊裏導出了一個 x2 變量,這個變量沒有被使用:

不開啓 scope hositing 的時候,生成的代碼是這樣的,它會被導出,只是沒被使用:

這時候如果你要刪掉它,你需要分析模塊之間的依賴關係,導出的變量哪些被使用了,哪些沒被使用。

還要保證這段代碼沒有副作用,才能把它刪除掉。

也就是 tree shaking 掉。

但如果 scope hositing 了之後呢?

這時你能很容易的分析出變量引用關係,然後把它刪掉。

這個都不用 webpack 做,直接用 TerserWebpackPlugin 這種壓縮的插件來做就行。

所以說,當實現了 scope hositing 之後,天然就支持了部分模塊的 tree shaking。

爲什麼說是部分呢?

因爲 scope hositing 也是有限制條件的。

可以在文檔裏看到這些,叫做 optimization bailouts,優化的退出條件,意思就是這些情況下不會做 scope hositing:

一個個來看:

Non ES6 Module 不會做 scope hoisting。

這個很容易理解,只有 es module 的依賴關係纔是能被分析的,都不是 es module 怎麼正確分析依賴關係,怎麼 hositing 呢?

export * from "cjs-module" 不會做 scope hositing

這個同上,一旦模塊引入了 cjs module,那就不可以分析依賴關係了,所以也就不能 hoisting。

use eval() 不會做 scope hositing

這個也容易理解,eval 的代碼你不能保證有啥東西,去掉 scope,合在一起很容易出問題。

using module 或者用了 ProvidePlugin 的變量,不會被 scope hositing。

用到了 module 變量之後,你合併成了一個模塊,那這個 module 不就沒了麼?

所以用了 module 變量不會被 hositing。

用了 ProvidePlugin 注入的變量也差不多。

In Multiple Chunks 不會被 scope hositing。

要是被多個 chunk 用到了,那 hositing 之後,代碼不就重複了多次麼?

所以只有被一個 chunk 用到的模塊纔會被 hositing 優化。

這些不會觸發 scope hositing 的情況倒是都挺容易理解的。

我們挑幾個來試一下:

比如我在 y 模塊用一個 eval:

跑下 webpack,這時生成的代碼是這樣的:

其他 3 個模塊都被 scope hositing 了,就是這個 y 變成了從別的模塊引入的方式。

可以看到在上面單獨定義了 y 模塊:

這就是規則裏說的,有 eval 的模塊不會觸發 scope hositing。

我們再來看看被多 chunk 引入的情況:

添加這樣一個 lazy 模塊,引入 x 模塊:

然後在 example 裏異步引入它:

異步引入的模塊是會被分到單獨的 chunk 的。

我們重新跑下 webpack 試試。

確實,lazy 的模塊單獨分了一個 chunk:

因爲 x 被 lazy 引用了,而 y 被 x 引用。

所以 scope hositing 是這樣做的:

example 和 a 被 hositing 到一個模塊了,而 x 單獨引入的。

而這個 x 模塊裏把 x、y 給 hosting 成一個模塊了:

這就是模塊在多個 chunk 時,會把它單獨摘出來,不會被 scope hositing。

知道了什麼是 scope hositing,什麼時候會觸發 scope hositing,哪些情況不會。

我們再來看看它的實現原理。

其實這個 optimization.concatenateModules 的配置不用自己開啓:

當你把 mode 設置爲 production 的時候,默認就會開啓這個選項。

而開啓這個選項的時候,內部會應用 ModuleConcatenationPlugin 這個插件:

也就是說 scope hositing 的功能就是 ModuleConcatenationPlugin 這個插件實現的。

webpack 的流程分爲 3 步:

make 是從入口模塊開啓,遞歸解析依賴,生成模塊依賴圖 ModuleGraph。

seal 階段是把 module 分到不同的 chunk,也就是分組,生成 ChunkGraph。

emit 階段把每個 chunk 使用模版打印出來,生成代碼,也就是 assets。

之後把 assets 寫入磁盤就好了。

ModuleConcatenationPlugin 這個插件在 optimizeChunkModules 這個 hook 生效,

這是 seal 階段的一個 hook:

這時候 moduleGraph、chunkGraph 都有了,可以從 compilation 對象裏拿到。

這個插件邏輯還是比較複雜的,我們理一下主流程好了:

這個插件會遍歷所有模塊,把不適合 scope hositing 的模塊過濾掉,同時記錄下不合適的原因。

最後剩下的有的是入口模塊,有的是其他的可以被 scope hositing 的模塊,分別放到兩個集合中:

然後從每個入口模塊開始,遞歸分析 import,把可以被 scope hositing 的模塊都放到這個 ConcatConfiguration 對象裏:

這個對象的作用就是記錄根模塊和子模塊:

這是一個可以被 scope hositing 的單位。

然後它會遍歷這些配置對象來創建一個個新的 module 對象:

這些新的 module 對象包含的子 module 都是可以被一起 scope hositing 的。

這是一個繼承了 webpack 的 Module 類的特殊的 Module 類。

然後把這個 module 包含的子 module 從之前的 chunk 裏刪掉,

之後把這個 module 替換成新的 module。

替換成 ConcatenatedModule 類型的新 module 對象有什麼用呢?

作用在代碼生成階段。

代碼生成的時候會調用每個 module 對象的 codeGeneration 方法:

而這個 module 對象是我們前面替換的 ConcatenatedModule 類型的,它重寫了 codeGeneration 方法

會遍歷模塊,根據類型分別打上 concatenated 和 external 的標記:

也就是是單獨一個模塊,還是合併到一起。

之後拼接代碼字符串的時候就會根據不同的類型做不同的處理。

對 concatenated 類型的模塊,還會對每個頂層的變量通過 AST 查找是否有同名變量,有的話就重命名。

拼接代碼的時候也是用不同的模版:

上面這段字符串是不是覺得眼熟?

沒錯,這部分就是在拼接最終生成的這種代碼:

可以對比下:

這樣我們就走完了這個插件的邏輯還有最終代碼生成的邏輯。

這就是 scope hoisting 的實現原理。

總結

scope hosiitng 可以把一些模塊的代碼合併成一個模塊作用域裏,這樣性能會更高,而且配合壓縮插件就可以實現 tree shaking。

同名的變量也不用擔心,scope hositing 的時候會做重命名。

當然,也不是所有的模塊都可以 scope hositing,有一些模塊不可以,主要是被多個 chunk 包含的模塊、有 cjs 代碼的模塊、有 eval 的模塊、用到了 module 變量的模塊。

這些類型的模塊不能被 scope hoisting 的原因也很容易理解,比如 cjs、eval 的代碼不能被分析、被多個 chunk 包含的模塊如果 hositing 會重複等等。

scope hositing 的功能需要開啓 optimization.concatenateModules 的配置項,或者設置 mode 爲 production,它的底層就是 ModuleConcatenationPlugin 這個插件。

webpack 分爲 make、seal、emit 3 個階段,這個插件在 seal 階段的 optimizeChunkModules 的 hook 生效。

它會遍歷模塊,根據規則過濾出可以被 scope hositing 的入口模塊和其他模塊,放到一個 ConcatConfiguration 對象裏。

然後遍歷這個對象,生成 ConcatenatedModule 類型的 module 替換之前的 module。

這樣當代碼生成階段,就會調用 ConcatenatedModule 的 codeGeneration 方法,這裏做了模塊類型的區分,同名變量的重命名,以及最終模塊代碼的拼接。

這樣生成的代碼就是 scope hositing 的代碼了。

這就是 scope hositing 的實現原理,它是 webpack 的基礎功能之一,而且也實現了部分 tree shaking。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/id-d8Cq_RW0HXc9DTVjAVA