目錄html
1.什麼是循環加載前端
「循環加載」簡單來講就是就是腳本之間的相互依賴,好比a.js
依賴b.js
,而b.js
又依賴a.js
。例如:node
// a.js const b = require('./b.js') // b.js const a = require('./a.js')
對於循環依賴,若是沒有處理機制,則會形成遞歸循環,而遞歸循環是應該被避免的。而且在實際的項目開發中,咱們很難避免循環依賴的存在,好比頗有可能出現a
文件依賴b
文件,b
文件依賴c
文件,c
文件依賴a
文件這種情形。es6
也所以,對於循環依賴問題,其解決方案不是不要寫循環依賴(沒法避免),而是從模塊化規範上提供相應的處理機制去識別循環依賴並作處理。api
接下來將介紹如今主流的兩種模塊化規範 CommonJS 模塊和 ES6 模塊是如何處理循環依賴以及它們有什麼差別。緩存
2.CommonJS 模塊的循環加載模塊化
CommonJS 模塊規範使用 require
語句導入模塊,module.exports
語句導出模塊。post
CommonJS 模塊是運行時加載:ui
運行時遇到模塊加載命令 require,就會去執行這個模塊,輸出一個對象(即
module.exports
屬性),而後再從這個對象的屬性上取值,輸出的屬性是一個值的拷貝,即一旦輸出一個值,模塊內部這個值發生了變化不會影響到已經輸出的這個值。code
CommonJS 的一個模塊,就是一個腳本文件。require
命令第一次加載該腳本,就會執行整個腳本,而後在內存生成一個對象。對於同一個模塊不管加載多少次,都只會在第一次加載時運行一次,以後再重複加載,就會直接返回第一次運行的結果(除非手動清除系統緩存)。
// module { id: '...', //模塊名,惟一 exports: { ... }, //模塊輸出的各個接口 loaded: true, //模塊的腳本是否執行完畢 ... }
上述代碼是一個 Node 的模塊對象,而用到這個模塊時,就會從對象的 exports
屬性中取值。
CommonJS 模塊解決循環加載的策略就是:一旦某個模塊被循環加載,就只輸出已經執行的部分,沒有執行的部分不輸出。
用一個 Node 官方文檔上的示例來說解其原理:
// 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);
main
腳本執行結果以下:
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 模塊支持)
實例一:
// 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
腳本,會發現直接報錯,以下圖:
簡單分析一下a
腳本執行過程:
① 開始執行a
腳本,加載b
腳本
② 進入b
腳本,加載a
腳本,發現了循環加載,此時不會再去執行a
腳本,只輸出已經執行的部分,但此時a
腳本中的foo
變量還未被初始化,接着輸出字符串a.mjs
,以後嘗試輸出foo
變量時,發現foo
變量還未被初始化,因此直接拋出異常
由於foo
變量是用let
關鍵字聲明的變量,let
關鍵字在執行上下文的建立階段,只會建立變量而不會被初始化(undefined),而且 ES6 規定了其初始化過程是在執行上下文的執行階段(即直到它們的定義被執行時才初始化),使用未被初始化的變量將會報錯。詳細瞭解let
關鍵字,能夠參考這篇文章深刻理解JS:var、let、const的異同。
實例二:用 var
代替 let
進行變量聲明。
// 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
腳本,將不會報錯,其結果以下:
這是由於使用 var 聲明的變量都會在執行上下文的建立階段時做爲變量對象的屬性被建立並初始化(undefined),因此加載b
腳本時,a
腳本中的foo
變量雖然沒有被賦值,但已經被初始化,因此不會報錯,能夠繼續執行。
4.小結
ES6 模塊與 CommonJS 模塊都不會再去執行重複加載的模塊,而且解決循環加載的策略也同樣:一旦某個模塊被循環加載,就只輸出已經執行的部分,沒有執行的部分不輸出。但因爲 CommonJS 模塊是運行時加載而 ES6 模塊是編譯時加載,因此也存在一些不一樣。
5.參考