1. 程式人生 > >前端模組化之迴圈載入

前端模組化之迴圈載入

**目錄** - 什麼是迴圈載入 - CommonJS 模組的迴圈載入 - ES6 模組的迴圈載入 - 小結 - 參考
**1.什麼是迴圈載入** “迴圈載入”簡單來說就是就是指令碼之間的相互依賴,比如`a.js`依賴`b.js`,而`b.js`又依賴`a.js`。例如: ```js // a.js const b = require('./b.js') // b.js const a = require('./a.js') ```
對於迴圈依賴,如果沒有處理機制,則會造成遞迴迴圈,而遞迴迴圈是應該被避免的。並且在實際的專案開發中,我們很難避免迴圈依賴的存在,比如很有可能出現`a`檔案依賴`b`檔案,`b`檔案依賴`c`檔案,`c`檔案依賴`a`檔案這種情形。
也因此,對於迴圈依賴問題,其解決方案不是不要寫迴圈依賴(無法避免),而是從模組化規範上提供相應的處理機制去識別迴圈依賴並做處理。
接下來將介紹現在主流的兩種模組化規範 CommonJS 模組和 ES6 模組是如何處理迴圈依賴以及它們有什麼差異。
**2.CommonJS 模組的迴圈載入** CommonJS 模組規範使用 `require` 語句匯入模組,`module.exports` 語句匯出模組。 CommonJS 模組是執行時載入: > 執行時遇到模組載入命令 require,就會去執行這個模組,輸出一個物件(即`module.exports`屬性),然後再從這個物件的屬性上取值,輸出的屬性是一個值的拷貝,即一旦輸出一個值,模組內部這個值發生了變化不會影響到已經輸出的這個值。
CommonJS 的一個模組,就是一個指令碼檔案。`require` 命令第一次載入該指令碼,就會執行整個指令碼,然後在記憶體生成一個物件。對於同一個模組無論載入多少次,都只會在第一次載入時執行一次,之後再重複載入,就會直接返回第一次執行的結果(除非手動清除系統快取)。 ```js // module { id: '...', //模組名,唯一 exports: { ... }, //模組輸出的各個介面 loaded: true, //模組的指令碼是否執行完畢 ... } ``` 上述程式碼是一個 Node 的模組物件,而用到這個模組時,就會從物件的 `exports`屬性中取值。
CommonJS 模組解決迴圈載入的策略就是:一旦某個模組被迴圈載入,就只輸出已經執行的部分,沒有執行的部分不輸出。
用一個 Node [官方文件](https://nodejs.org/api/modules.html#modules_cycles)上的示例來講解其原理: ```js // 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'); ``` ```js // 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'); ``` ```js // 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); ``` `main`指令碼執行結果如下: ![main指令碼執行結果](https://img2020.cnblogs.com/blog/898684/202007/898684-20200712201142933-1519753097.png) `main`指令碼 執行的順序如下: ① 輸出字串 `main starting` 後,載入`a`指令碼 ② 進入 `a` 指令碼,`a`指令碼中輸出的`done`變數被設定為`false`,隨後輸出字串 `a starting`,然後載入 `b`指令碼 ③ 進入 `b` 指令碼,隨後輸出字串 `b starting`,接著`b`指令碼中輸出的`done`變數被設定為`false`,然後載入 `a`指令碼,發現了**迴圈載入**,此時不會再去執行`a`指令碼,只輸出已經執行的部分(即輸出`a`指令碼中的變數`done`,此時其值為`false`),隨後輸出字串`in b, a.done = false`,接著`b`指令碼中輸出的`done`變數被設定為`true`,最後輸出字串 `b done`,`b`指令碼執行完畢,回到之前的`a`指令碼 ④ `a`指令碼繼續從第4行開始執行,隨後輸出字串`in a, b.done = true`,接著`a`指令碼中輸出的`done`變數被設定為`true`,最後輸出字串 `a done`,`a`指令碼執行完畢,回到之前的`main`指令碼 ⑤ `main`指令碼繼續從第3行開始執行,載入`b`指令碼,發現`b`指令碼**已經被載入**了,將不再執行,直接返回之前的結果,最終輸出字串`in main, a.done = true, b.done = true`,至此`main`指令碼執行完畢
**3.ES6 模組的迴圈載入** ES6 模組規範使用 `import` 語句匯入模組中的變數,`export` 語句匯出模組中的變數。 ES6 模組是編譯時載入: > 編譯時遇到模組載入命令 import,不會去執行這個模組,只會輸出一個只讀引用,等到真的需要用到這個值時(即執行時),再通過這個引用到模組中取值。換句話說,模組內部這個值改變了,仍舊可以根據輸出的引用獲取到最新變化的值。
跟 CommonJS 模組一樣,ES6 模組也不會再去執行重複載入的模組,並且解決迴圈載入的策略也一樣:一旦某個模組被迴圈載入,就只輸出已經執行的部分,沒有執行的部分不輸出。
但ES6 模組的迴圈載入與 CommonJS 存在本質上的不同。由於 ES6 模組是動態引用,用 `import`從一個模組載入變數,那個變數不會被快取(是一個引用),所以只需要保證真正取值時能夠取到值,即已經宣告初始化,程式碼就能正常執行。
以下程式碼示例,是用 Node 來載入 ES6 模組,所以使用`.mjs`字尾名。(從Node v13.2 版本開始,才預設打開了 ES6 模組支援)
例項一: ```js // a.mjs import { bar } from './b'; console.log('a.mjs'); console.log(bar); export let foo = 'foo'; // b.mjs import { foo } from './a'; console.log('b.mjs'); console.log(foo); export let bar = 'bar'; ``` 執行 `a`指令碼,會發現直接報錯,如下圖: ![丟擲異常](https://img2020.cnblogs.com/blog/898684/202007/898684-20200712201210854-468440363.png) 簡單分析一下`a`指令碼執行過程: ① 開始執行`a`指令碼,載入`b`指令碼 ② 進入`b`指令碼,載入`a`指令碼,發現了**迴圈載入**,此時不會再去執行`a`指令碼,只輸出已經執行的部分,但此時`a`指令碼中的`foo`變數還未被初始化,接著輸出字串`a.mjs`,之後嘗試輸出`foo`變數時,發現`foo`變數還未被初始化,所以直接丟擲異常
因為`foo`變數是用`let`關鍵字宣告的變數,`let`關鍵字在執行上下文的建立階段,只會建立變數而不會被初始化(undefined),並且 ES6 規定了其初始化過程是在執行上下文的執行階段(即直到它們的定義被執行時才初始化),使用未被初始化的變數將會報錯。詳細瞭解`let`關鍵字,可以參考這篇文章[深入理解JS:var、let、const的異同](https://www.cnblogs.com/forcheng/p/13033976.html)。
例項二:用 `var` 代替 `let` 進行變數宣告。 ```js // a.mjs import { bar } from './b'; console.log('a.mjs'); console.log(bar); export var foo = 'foo'; // b.mjs import { foo } from './a'; console.log('b.mjs'); console.log(foo); export var bar = 'bar'; ``` 執行 `a`指令碼,將不會報錯,其結果如下: ![正常執行](https://img2020.cnblogs.com/blog/898684/202007/898684-20200712201230570-1895633055.png) 這是因為使用 var 宣告的變數都會在執行上下文的建立階段時作為變數物件的屬性被建立並初始化(undefined),所以載入`b`指令碼時,`a`指令碼中的`foo`變數雖然沒有被賦值,但已經被初始化,所以不會報錯,可以繼續執行。
**4.小結** ES6 模組與 CommonJS 模組都不會再去執行重複載入的模組,並且解決迴圈載入的策略也一樣:一旦某個模組被迴圈載入,就只輸出已經執行的部分,沒有執行的部分不輸出。但由於 CommonJS 模組是執行時載入而 ES6 模組是編譯時載入,所以也存在一些不同。
**5.參考** [前端模組化:CommonJS,AMD,CMD,ES6](https://juejin.im/post/5aaa37c8f265da23945f365c) [你可能不知道的 JavaScript 模組化野史](https://juejin.im/post/5e3985396fb9a07cde64c489) [Module 的載入實現](https://es6.ruanyifeng.com/#docs/module