從 Nodejs 如何解決模塊循環依賴問題來一點關於模塊的發散思考

什麼是模塊循環依賴

所謂循環依賴就如字面理解的那樣, 衆所周知 Nodejs 對模塊的解析是會提早加載到內存中, 當全部模塊加載完才從入口文件開始 run 整個應用, 若是全部的模塊之間都是按照順序串行依賴, 就跟貪吃蛇同樣是沒什問題, 無非就是依賴鏈長一點, 不過要是貪吃蛇不當心咬到了身體或者尾巴, 就會行程環, 在代碼中就比如前端

//a.js
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');

//b.js
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');

//main.js
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
複製代碼

這是一個 Nodejs 官網的示例, 正如示例中的那樣, a.js 巴拉巴拉, 讀到 require b.js, 而後跑去 b.js, b.js 巴拉巴拉 又讀到 require a.js, 而後跑去 a.js, 這時候咱們不妨想一想第二次讀取 a.js 的時候 Nodejs 該怎麼處理呢, 若是像瀏覽器同樣的話, 應該重頭再解析一遍, 畢竟哪有半途而廢的道理嘛, 因而從頭再讀...巴拉巴拉 又讀到 require b.js 跑進去再重來, 等等這不是又回來了!?node

這就是模塊循環依賴問題瀏覽器

要解決這個問題, 提及來也很簡單, 只要把重來一遍變成中斷就能夠了, Nodejs 稱爲 unfinished copy, 第二進入 a.js 模塊的時候, 從require b.js 的後面繼續往下讀取, 這樣就將環解開又回到了原先串行的解析方式, 代價就是你得知道 require 先後都寫了什麼, 尤爲是涉及 exports 出去的值, 由於執行順序問題, 兩次的值並不相同. 不過若是咱們不想中斷, 就想從頭至尾讀呢?網絡

那咱們就須要將模塊解析和模塊加載分開處理, 好比咱們 ES6 Module 帶來的 import數據結構

//a.js
import b from 'b.js'
import c from 'c.js'

...coding...
//b.js
import a from 'a.js'

...coding...
複製代碼

由於代碼的執行分紅了兩個階段, 意味着不管你 import 寫在哪都會比其餘代碼先被讀取, 這也就解決了 require 先後代碼執行順序致使 exports 值不一致的問題, 因此爲啥說要 import 寫在頂上, 那是爲了符合直覺, 由於順序就是從 import 先開始的, 因此從這個角度看若是 require 的設計撇開 module 這個概念, 好比 require 的不是 module 而是嗯片斷, 就比較符合直覺, 由於是個超級大的 script , 可是若是加上 module 就有點反直覺, 由於咱們都會以爲 module 和 module 之間是隔離的, 一個 module 的加載若是由於 require 致使被割裂...就感受這種設計好殘疾, 與其說是 unfinished copy 不如說是 unfinished module. 因此用 copy 代替 module, 叫 CommonJS Copy 可能會更符合實際的設計.函數

回到正題, 經過解析加載分離, 咱們就能夠先解析模塊的依賴關係, 而避免去讀取實際的模塊代碼, 在 a.js 中咱們讀取到 import 'b.js', 這時候咱們能夠給 b.js 添加一個 State, 並將其值設爲 'Linked', 表示 b.js 已經被讀取了, 而後從 b.js 讀取到 import a.js, 咱們再將 a.js 的 State 設爲 'Linked', 而後又回到 a.js, 發現 b.js 已是 Linked 了, 跳過, 繼續讀取 c.js , 經過狀態標記就避免了循環依賴學習

Import 帶來的禮物 Tree-Shaking

Tree-Shaking 是 rollup 提出的一個移除 dead code 的方案, 這裏的搖樹的概念應該是源自於 rollup 的具體處理方式, rollup 將代碼轉換成一顆 AST 而後搖啊搖, 其實就是不斷的移除那些 unuesed 的變量/函數/類等等, 因此叫樹搖...吧, 而之因此能這麼作的前提其實就是基於 Import 的靜態分析, 基於靜態分析, 就能夠先分析出 module 自己導出的對象, 和全部被其餘 module 引入的對象, 找出那些導出未使用的變量, 而後再從 module 自身分析下是否有使用這些變量, 若是都沒有, 那就搖掉吧...並且搖樹背後其實意有所指, 好比當咱們使勁搖樹的時候會掉什麼!?測試

天然是 ---- 葉子...優化

在 AST 中掉下來的葉子就是那些 node 節點, 因此搖樹, 或者樹搖是否是很形象 😁ui

不過話說回來, 實現 Tree-Shaking 其實用到了兩種基本的數據解構, 圖和樹, 因此學好數據結構仍是頗有必要的 😀 (說到這裏, 我已經回去重修了... 😢)

CommonJS -> ES6 Module

Import 的設計比 require 更進一步, 不只解決了問題還引入可優化的方案, 在 Import 的基礎上得以發展出 Tree-Shaking 這樣的技術來進一步優化構建, 這是技術進步的意義所在, 並且 Import 還爲遠程模塊留下了擴展支持, 像 Deno 那樣 Import 一個遠程模塊, 若是發生在運行時那估計會比較糟糕, 若是加載的某個模塊不可用, 會影響關聯的全部模塊, 對於大型應用而言這幾乎是在災難性的, 由於網絡環境自己是不穩定的, 加上網絡環境的不可知性, 會讓你的模塊系統陷入不可用的窘境, 但若是是靜態分析, 咱們能夠提早測試模塊的可用性, 網絡的介入多少會讓問題變複雜, 而依賴分析和加載的解耦對於這種場景也可以很好的去支持, 因此 Import 基於靜態分析的規範設計實際上是很是棒的, 像這種支持將來的設計都是很棒的設計!? (因此值得咱們好好學習, 陷入深深的思考)

後話

ES6 Module 是很棒的設計, 不過寫這篇文章的時候難免稍稍有點傷感, 由於這類關於底層的標準設計大多數都是外國人, 嗯基本上是外國人, 在國內其實不多有碰到關於這類底層標準/規範設計的討論, 好比 require 的實現或者說 CommonJS Module 的設計有缺陷, 但其實鮮有人關注, 或者有人關注了但不多引起討論, 咱們彷佛已經習慣了接受某種嗯廣爲人知的設計或者接受某種已經存在的標準, 而不多獨立思考, 甚至去質疑, 質疑爲何這麼設計

有時候我會以爲前端圈像個投資圈, 咱們彷佛再不停的追逐一個又一個技術風口, 以致於誕生了有名的調侃 "學不動了" 其實這種調侃背後是咱們對不斷追逐技術風口感到疲憊, 由於大多數人並無所以受益, 就好像投資圈賺錢的永遠是那幾個大佬, 普通老百姓都是韭菜, 不信? 你看看股市裏的幣圈裏的韭菜們就知道了

或許咱們能夠停下來, 思考一些東西, 質疑一些東西, 而不只僅是追逐.

相關文章
相關標籤/搜索