"循環加載"(circular dependency)指的是,<code>a</code>腳本的執行依賴<code>b</code>腳本,而<code>b</code>腳本的執行又依賴<code>a</code>腳本。
通常,"循環加載"表示存在強耦合,如果處理不好,還可能導緻遞歸加載,使得程式無法執行,是以應該避免出現。

但是實際上,這是很難避免的,尤其是依賴關系複雜的大項目,很容易出現<code>a</code>依賴b,<code>b</code>依賴<code>c</code>,<code>c</code>又依賴<code>a</code>這樣的情況。這意味着,子產品加載機制必須考慮"循環加載"的情況。
本文介紹javascript語言如何處理"循環加載"。目前,最常見的兩種子產品格式commonjs和es6,處理方法是不一樣的,傳回的結果也不一樣。
介紹es6如何處理"循環加載"之前,先介紹目前最流行的commonjs子產品格式的加載原理。
commonjs的一個子產品,就是一個腳本檔案。<code>require</code>指令第一次加載該腳本,就會執行整個腳本,然後在記憶體生成一個對象。
以後需要用到這個子產品的時候,就會到<code>exports</code>屬性上面取值。即使再次執行<code>require</code>指令,也不會再次執行該子產品,而是到緩存之中取值。
commonjs子產品的重要特性是加載時執行,即腳本代碼在<code>require</code>的時候,就會全部執行。commonjs的做法是,一旦出現某個子產品被"循環加載",就隻輸出已經執行的部分,還未執行的部分不會輸出。
上面代碼之中,<code>a.js</code>腳本先輸出一個<code>done</code>變量,然後加載另一個腳本檔案<code>b.js</code>。注意,此時<code>a.js</code>代碼就停在這裡,等待<code>b.js</code>執行完畢,再往下執行。
再看<code>b.js</code>的代碼。
上面代碼之中,<code>b.js</code>執行到第二行,就會去加載<code>a.js</code>,這時,就發生了"循環加載"。系統會去<code>a.js</code>子產品對應對象的<code>exports</code>屬性取值,可是因為<code>a.js</code>還沒有執行完,從<code>exports</code>屬性隻能取回已經執行的部分,而不是最後的值。
<code>a.js</code>已經執行的部分,隻有一行。
是以,對于<code>b.js</code>來說,它從<code>a.js</code>隻輸入一個變量<code>done</code>,值為<code>false</code>。
然後,<code>b.js</code>接着往下執行,等到全部執行完畢,再把執行權交還給<code>a.js</code>。于是,<code>a.js</code>接着往下執行,直到執行完畢。我們寫一個腳本<code>main.js</code>,驗證這個過程。
執行<code>main.js</code>,運作結果如下。
上面的代碼證明了兩件事。一是,在<code>b.js</code>之中,<code>a.js</code>沒有執行完畢,隻執行了第一行。二是,<code>main.js</code>執行到第二行時,不會再次執行<code>b.js</code>,而是輸出緩存的<code>b.js</code>的執行結果,即它的第四行。
es6子產品的運作機制與commonjs不一樣,它遇到子產品加載指令<code>import</code>時,不會去執行子產品,而是隻生成一個引用。等到真的需要用到時,再到子產品裡面去取值。
是以,es6子產品是動态引用,不存在緩存值的問題,而且子產品裡面的變量,綁定其所在的子產品。請看下面的例子。
上面代碼中,<code>m1.js</code>的變量<code>foo</code>,在剛加載時等于<code>bar</code>,過了500毫秒,又變為等于<code>baz</code>。
讓我們看看,<code>m2.js</code>能否正确讀取這個變化。
上面代碼表明,es6子產品不會緩存運作結果,而是動态地去被加載的子產品取值,以及變量總是綁定其所在的子產品。
這導緻es6處理"循環加載"與commonjs有本質的不同。es6根本不會關心是否發生了"循環加載",隻是生成一個指向被加載子產品的引用,需要開發者自己保證,真正取值的時候能夠取到值。
按照commonjs規範,上面的代碼是沒法執行的。<code>a</code>先加載<code>b</code>,然後<code>b</code>又加載<code>a</code>,這時<code>a</code>還沒有任何執行結果,是以輸出結果為<code>null</code>,即對于<code>b.js</code>來說,變量<code>foo</code>的值等于<code>null</code>,後面的<code>foo()</code>就會報錯。
但是,es6可以執行上面的代碼。
<code>a.js</code>之是以能夠執行,原因就在于es6加載的變量,都是動态引用其所在的子產品。隻要引用是存在的,代碼就能執行。
上面代碼中,<code>even.js</code>裡面的函數<code>foo</code>有一個參數<code>n</code>,隻要不等于0,就會減去1,傳入加載的<code>odd()</code>。<code>odd.js</code>也會做類似操作。
運作上面這段代碼,結果如下。
上面代碼中,參數<code>n</code>從10變為0的過程中,<code>foo()</code>一共會執行6次,是以變量<code>counter</code>等于6。第二次調用<code>even()</code>時,參數<code>n</code>從20變為0,<code>foo()</code>一共會執行11次,加上前面的6次,是以變量<code>counter</code>等于17。
這個例子要是改寫成commonjs,就根本無法執行,會報錯。
上面代碼中,<code>even.js</code>加載<code>odd.js</code>,而<code>odd.js</code>又去加載<code>even.js</code>,形成"循環加載"。這時,執行引擎就會輸出<code>even.js</code>已經執行的部分(不存在任何結果),是以在<code>odd.js</code>之中,變量<code>even</code>等于<code>null</code>,等到後面調用<code>even(n-1)</code>就會報錯。
(完)