Commonjs唤锉、esm世囊、Amd 和 Cmd 的循環(huán)依賴表現(xiàn)和原理

當(dāng) a 模塊執(zhí)行時(shí)依賴 b 模塊,b 模塊的執(zhí)行又反過來依賴 a 模塊窿祥,此時(shí)就發(fā)生了循環(huán)依賴株憾。循環(huán)依賴在平常的業(yè)務(wù)代碼里比較罕見,一般遇到就意味著代碼架構(gòu)是時(shí)候認(rèn)真梳理一下了。

但在依賴關(guān)系復(fù)雜的系統(tǒng)里嗤瞎,是有可能出現(xiàn)循環(huán)依賴的情況墙歪。讓我們一起來看看在 Commonjsnodejs)、ES module贝奇、AmdRequireJS)和 CmdSeajs)各種主流模塊標(biāo)準(zhǔn)下的循環(huán)依賴表現(xiàn)及其背后的原理虹菲。

Commonjs

我們來看看node官方文檔里提供的 循環(huán)依賴demo

// 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);

執(zhí)行 main.js掉瞳,輸出如下:

$ node main.js

main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true

這里在執(zhí)行 a.js 時(shí)届惋,依賴 b.js,而執(zhí)行 b.js 時(shí)菠赚,反過來又依賴 a.js 的輸出,造成了循環(huán)依賴郑藏,然而程序并不會(huì)陷入無限循環(huán)衡查,這里到底發(fā)生了什么?根據(jù)官方原文:

In order to prevent an infinite loop, an unfinished copy of the a.js exports object is returned to the b.js module.

翻譯過來就是必盖,模塊被循環(huán)依賴時(shí)拌牲,只會(huì)輸出當(dāng)前執(zhí)行完成的導(dǎo)出值。也就是說歌粥,b.js 在依賴未執(zhí)行完成的 a.js 時(shí)塌忽,并不會(huì)等待 a.js 執(zhí)行完,而是直接輸出當(dāng)前執(zhí)行過的 export 對(duì)象失驶,也就是例程中的第二行:

// a.js
exports.done = false;

除此之外土居,我們還注意到一點(diǎn),main.js 在執(zhí)行 require('./b.js') 時(shí)嬉探,為什么 log 都沒打印出來擦耀?很顯然 node 在這里做了緩存,而且緩存時(shí)機(jī)必須是模塊執(zhí)行完成之后涩堤,畢竟 main.js 最后輸出的 a.doneb.done 都是 true眷蜓。


ES module

關(guān)于 ES module 的循環(huán)依賴表現(xiàn),我這里提供了2個(gè)比較有代表性的 demo胎围,都是運(yùn)行在 node 端吁系。

demo1

// a.mjs
console.log('a starting');
export default {
  done: true,
}
import b from './b.mjs';
console.log('in a, b.done = %j', b.done);
console.log('a done');
// b.mjs
console.log('b starting');
export default {
  done: true,
}
import a from './a.mjs';
console.log('in b, a.done = %j', a.done);
console.log('b done');

執(zhí)行 a.mjs,輸出如下

$ node --experimental-modules a.mjs

b starting
ReferenceError: a is not defined

如果 ES moduleCommonjs 一樣都是運(yùn)行時(shí)加載/導(dǎo)出白魂,那么按照 js 代碼的執(zhí)行順序汽纤,b.mjs 讀取 a.done 時(shí)不應(yīng)該拋出 undefined 異常;另外碧聪,雖然入口模塊是 a.mjs冒版,但先打印出的是 b starting,所以不難猜想:

ES module 不是動(dòng)態(tài)解析逞姿,且依賴模塊優(yōu)先執(zhí)行

demo2

// a.mjs
import b from './b.mjs';
console.log('a starting');
console.log(b());
export default function () {
  return 'run func A';
}
console.log('a done');
// b.mjs
import a from './a.mjs';
console.log('b starting');
console.log(a());
export default function () {
  return 'run func B';
}
console.log('b done');

執(zhí)行 a.mjs辞嗡,輸出如下

$ node --experimental-modules a.mjs

b starting
run func A
b done
a starting
run func B
a done

啥情況捆等?怎么把導(dǎo)出對(duì)象 object 改為導(dǎo)出函數(shù) function 就不會(huì)報(bào) undefined 異常?接下來讓我們帶著以上的結(jié)論和問題來探究 ES module 原理续室。

ES module 原理

這里只會(huì)簡(jiǎn)短闡述原理栋烤,詳見 ES modules: A cartoon deep-dive。實(shí)際上 ES module 從加載入口模塊到所有模塊實(shí)例的執(zhí)行主要經(jīng)歷了三步:構(gòu)建挺狰、實(shí)例化運(yùn)行明郭。

  • 構(gòu)建

從入口模塊開始,根據(jù) import 關(guān)鍵字遍歷依賴樹丰泊,每遍歷一個(gè)模塊則生成該模塊的 模塊記錄(module record)薯定,最后生成整個(gè) 模塊圖譜(module graph)

解析模塊生成模塊記錄

注意瞳购,這一步是 ES moduleCommonjs 的本質(zhì)區(qū)別:

因?yàn)?ES module 需要支持瀏覽器端话侄,而構(gòu)建過程要獲取所有的模塊文件來繪制模塊依賴圖譜,如果參考 Commonjs 的做法把模塊解析和運(yùn)行放在一起学赛,那么冗長(zhǎng)的下載過程將會(huì)嚴(yán)重阻塞主線程導(dǎo)致應(yīng)用長(zhǎng)時(shí)間不可用年堆,所以 ES module 在構(gòu)建過程不會(huì)實(shí)例化和執(zhí)行任何的js代碼,也就是所謂的 靜態(tài)解析 過程盏浇。

這同時(shí)也解釋了為何不支持使用表達(dá)式/變量的 import 語句:

// 報(bào)錯(cuò)
let module = 'my_module';
import { foo } from module;

所有的模塊記錄都會(huì)被緩存在 模塊映射(module map) 中变丧,被依賴多次的模塊也只會(huì)存在唯一一條映射記錄,從而避免模塊的重復(fù)下載和實(shí)例化绢掰。

模塊映射
  • 實(shí)例化

根據(jù)模塊記錄的關(guān)系痒蓬,在內(nèi)存中把模塊的導(dǎo)入 import 和導(dǎo)出 export 連接在一起,也稱為 活綁定(live bindings)滴劲。

JS引擎會(huì)為每個(gè)模塊記錄創(chuàng)建 模塊環(huán)境記錄(module environment record)谊却,用來關(guān)聯(lián)模塊實(shí)例和模塊的導(dǎo)入/導(dǎo)出值。引擎會(huì)先采用 深度優(yōu)先后序遍歷(depth first post-order traversal)哑芹,將模塊及其依賴的導(dǎo)出 export 連接到內(nèi)存中(直到依賴樹末端)炎辨,然后逐層返回再把模塊相對(duì)應(yīng)的導(dǎo)入 import 連接到內(nèi)存的同一位置。這也解釋了為什么導(dǎo)出模塊的值變更時(shí)聪姿,導(dǎo)入模塊也能捕捉到該值的變更碴萧。

模塊實(shí)例通過導(dǎo)入/導(dǎo)出變量在內(nèi)存中建立關(guān)系

需要注意的是,實(shí)例化只是JS引擎在內(nèi)存中綁定模塊間關(guān)系末购,并沒有執(zhí)行任何代碼破喻,也就是說這些連接好的內(nèi)存空間中并沒有存儲(chǔ)變量值,然而盟榴,在此過程中導(dǎo)出函數(shù)將會(huì)被初始化曹质,即所謂的 函數(shù)具有提升作用

這使循環(huán)依賴的問題自然而然地被解決:

JS引擎不需要關(guān)心是否存在循環(huán)依賴,只需要在代碼運(yùn)行的時(shí)候羽德,從內(nèi)存空間中讀取該導(dǎo)出值几莽。

我們回到上面提供的 ES module 循環(huán)依賴的例程。

第一個(gè)例程 b.mjs 模塊(簡(jiǎn)稱 b 模塊)在獲取 a.mjs 模塊(簡(jiǎn)稱 a 模塊)的導(dǎo)出值時(shí)宅静,a 模塊的對(duì)象 { done: true } 并沒有被聲明和賦值章蚣,所以會(huì)拋出 undefined 異常。

第二個(gè)例程姨夹,由于函數(shù)具有提升作用纤垂,b 模塊獲取 a 模塊導(dǎo)出值時(shí),a 模塊的 foo 函數(shù)已經(jīng)被聲明磷账,不會(huì)拋出異常峭沦。

  • 運(yùn)行

也就是往內(nèi)存空間中填充真實(shí)值。

JS引擎會(huì)采用和實(shí)例化時(shí)一樣的深度優(yōu)先后序遍歷來執(zhí)行模塊及其依賴的頂級(jí)代碼(即除函數(shù)聲明之外的代碼)逃糟,所以會(huì)出現(xiàn) demo1 中的 log 順序熙侍。

nodejs 已經(jīng)實(shí)現(xiàn)了對(duì) ES module 的支持,目前只是作為一個(gè)實(shí)驗(yàn)特性履磨,我會(huì)找時(shí)間研究 node 實(shí)現(xiàn) CommonjsES module 的底層源碼,大家敬請(qǐng)期待庆尘。


RequireJS

RequireJSSeajs 都是主要針對(duì)瀏覽器端的模塊加載器剃诅,模塊加載流程離不開這幾點(diǎn):

  1. 根據(jù)加載器規(guī)則尋找模塊,并通過插入script標(biāo)簽異步加載驶忌;
  2. 在模塊代碼中通過詞法分析找出依賴模塊并加載矛辕,遞歸此過程直到依賴樹末端;
  3. 綁定 load 事件付魔,當(dāng)依賴模塊都加載完成時(shí)執(zhí)行回調(diào)函數(shù)聊品;

當(dāng)然加載器還涉及緩存機(jī)制、容錯(cuò)處理和一些復(fù)雜的配置等几苍,有興趣的同學(xué)可以看看源碼自行研究翻屈,這里就不詳細(xì)說了。

這里我們把 Commonjs 的 demo 稍微改動(dòng)下妻坝,使其運(yùn)行在瀏覽器端:

<!-- index.html  -->
<html>
  <body>
    <script data-main="./app.js" src="./require.js"></script>
  </body>
</html>
// app.js
define(['./a', './b'], function(a, b) {
  console.log('app starting');
  console.log('in app', a, b);
});
// a.js
define(['./b', 'exports'], function(b, exports) {
  console.log('a starting');
  exports.done = false;
  console.log('in a, b.done =', b.done);
  console.log('a done');
  exports.done = true;
});
// b.js
define(['./a', 'exports'], function(a, exports) {
  console.log('b starting');
  exports.done = false;
  console.log('in b, a.done =', a.done);
  console.log('b done');
  exports.done = true;
});

啟動(dòng) http-server:

# npm install -g http-server
$ http-server

打開 chrome伸眶,查看 console 控制臺(tái)輸出:

b starting
b.js:4 in b, a.done = undefined
b.js:5 b done
a.js:2 a starting
a.js:4 in a, b.done = true
a.js:5 a done
app.js:2 app starting
app.js:3 in app {done: true} {done: true}

首先打印的是 b 模塊中的 console.log('b starting'),而不是 app 模塊中的 console.log('app starting')刽宪,可以看出 Requirejs 是遵循 依賴前置 原則:demo 中 a 模塊依賴 b 模塊厘贼,在 a 模塊回調(diào)執(zhí)行前,會(huì)先確保 b 模塊執(zhí)行完畢圣拄,所以 b 模塊中 a.done = undefined嘴秸。需要注意的是,如果不使用 exports 包來導(dǎo)出模塊返回值而選擇直接 return 的話,b 模塊中訪問 a 模塊導(dǎo)出值將會(huì)報(bào) undefined 異常岳掐,相當(dāng)于說 exports 包為模塊的導(dǎo)出預(yù)置了一個(gè)空對(duì)象(詳見 RequireJS API)凭疮。

所以 RequireJS 在解決循環(huán)依賴時(shí),假設(shè)模塊都沒有執(zhí)行過(沒有緩存記錄)的前提下岩四,總會(huì)有其中一個(gè)模塊讀取依賴值是 空對(duì)象 或者 undefined哭尝。


Seajs

那么同樣的 demo 運(yùn)行在 Seajs 框架下是什么效果呢?稍微改動(dòng)下代碼使其符合 Cmd 規(guī)范:

<!-- index.html -->
<html>
  <body>
    <script src="./sea.js"></script>
    <script>
      seajs.use('./app.js');
    </script>
  </body>
</html>
// app.js
define(function(require) {
  var a = require('./a');
  var b = require('./b');
  console.log(a, b);
});
// a.js
define(function(require, exports) {
  console.log('a starting');
  exports.done = false;
  var b = require('./b');
  console.log('in a, b.done =', b.done);
  console.log('a done');
  exports.done = true;
});
// b.js
define(function(require, exports) {
  console.log('b starting');
  exports.done = false;
  var a = require('./a');
  console.log('in b, a.done =', a.done);
  console.log('b done');
  exports.done = true;
});

控制臺(tái)輸出:

app.js:2 app starting
a.js:2 a starting
b.js:2 b starting
b.js:5 in b, a.done = false
b.js:6 b done
a.js:5 in a, b.done = true
a.js:6 a done
app.js:5 in app {done: true} {done: true}

RequireJS 的 log 不一樣(但和 Commonjs 的 demo 輸出完全一致)剖煌,這里是先打印 app starting材鹦,印證了 Seajs 所遵循的 依賴就近 原則,就是模塊只有在被 require 的時(shí)候才會(huì)執(zhí)行耕姊。所以 SeajsCommonjs 解決循環(huán)依賴的辦法都是一樣的簡(jiǎn)單粗暴桶唐,需要的時(shí)候就去緩存中實(shí)時(shí)取副本,取到什么就是什么茉兰。

無論是哪一種規(guī)范尤泽,都沒有局限于在哪一端運(yùn)行,譬如 CommonjsES module 都支持在 node 端或?yàn)g覽器端運(yùn)行规脸。為了解決各大瀏覽器對(duì)于這些模塊化標(biāo)準(zhǔn)的支持度不一的問題坯约,我們一般使用 webpack、browserify 等構(gòu)建工具處理模塊代碼莫鸭,下一期會(huì)著重講解 webpack 是如何實(shí)現(xiàn) CommonjsES module 等模塊標(biāo)準(zhǔn)的闹丐。

PS:本文章涉及的所有 demo 已放在 github 上。

Reference

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末被因,一起剝皮案震驚了整個(gè)濱河市卿拴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌梨与,老刑警劉巖堕花,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異粥鞋,居然都是意外死亡缘挽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門呻粹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來到踏,“玉大人,你說我怎么就攤上這事尚猿∥迅澹” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵凿掂,是天一觀的道長(zhǎng)伴榔。 經(jīng)常有香客問我纹蝴,道長(zhǎng),這世上最難降的妖魔是什么踪少? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任塘安,我火速辦了婚禮,結(jié)果婚禮上援奢,老公的妹妹穿的比我還像新娘兼犯。我一直安慰自己,他們只是感情好集漾,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布切黔。 她就那樣靜靜地躺著,像睡著了一般具篇。 火紅的嫁衣襯著肌膚如雪纬霞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天驱显,我揣著相機(jī)與錄音诗芜,去河邊找鬼。 笑死埃疫,一個(gè)胖子當(dāng)著我的面吹牛伏恐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播栓霜,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼翠桦,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了叙淌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤愁铺,失蹤者是張志新(化名)和其女友劉穎鹰霍,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體茵乱,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡茂洒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瓶竭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片督勺。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖斤贰,靈堂內(nèi)的尸體忽然破棺而出智哀,到底是詐尸還是另有隱情,我是刑警寧澤荧恍,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布瓷叫,位于F島的核電站屯吊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏摹菠。R本人自食惡果不足惜盒卸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望次氨。 院中可真熱鬧蔽介,春花似錦、人聲如沸煮寡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽洲押。三九已至武花,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間杈帐,已是汗流浹背体箕。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留挑童,地道東北人累铅。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像站叼,于是被迫代替她去往敵國(guó)和親娃兽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345