Node.js的循環(huán)依賴
循環(huán)依賴,簡(jiǎn)單點(diǎn)來(lái)說(shuō)就是a文件中require b文件,然后b文件中又反過(guò)來(lái)require a文件。這個(gè)問(wèn)題我們平時(shí)可能并不大注意到,但如果處理不好可能會(huì)引起一些讓人摸不清的問(wèn)題。在node中,是如何處理循環(huán)依賴的問(wèn)題的呢?
51CTO推薦專題:Node.js專區(qū)
寫個(gè)簡(jiǎn)單的例子來(lái)試驗(yàn)一下看吧。
定義兩個(gè)文件:
a.js
- var b = require('./b');
- console.log('a.js get b:' + b.b);
- module.exports.a = 1;
b.js
- var a = require('./a');
- console.log('b.js get a:' + a.a);
- module.exports.b = 2;
執(zhí)行
node a.js
輸出的結(jié)果是
b.js get a:undefined
a.js get b:2
從打印的軌跡上來(lái)看,代碼執(zhí)行的流程大致如下:
- a.js: b.js:
- var b = require('./b');
- var a = require('./a'); // a = {}
- console.log('b.js get a:' + a.a);
- module.exports.b = 2;
- // b = {b: 2}
- console.log('a.js get b:' + b.b);
- module.exports.a = 1;
node的加載過(guò)程,可以在lib/module.js文件中找到。與這個(gè)過(guò)程相關(guān)的代碼主要集中在Module._load方法里??梢钥吹剑琻ode會(huì)為每個(gè)新加載的文件創(chuàng)建一個(gè)Module對(duì)象(假設(shè)為a),這個(gè)就是我們?cè)赼.js代碼中看到的module了。在創(chuàng)建a之后,node會(huì)先將a放到cache中,然后再對(duì)它進(jìn)行加載操作。也就是說(shuō),如果在加載a的過(guò)程中,有其他的代碼(假設(shè)為b)require a.js的話,那么b可以從cache中直接取到a的module,從而不會(huì)引起重復(fù)加載的死循環(huán)。但帶來(lái)的代價(jià)就是在load過(guò)程中,b看到的是不完整的a,也就是為什么前面打印undefined的原因。
Module的構(gòu)造函數(shù)
- function Module(id, parent) {
- this.id = id;
- this.exports = {};
- this.parent = parent;
- this.filename = null;
- this.loaded = false;
- this.exited = false;
- this.children = [];
- }
Module._load方法
- Module._load = function(request, parent, isMain) {
- //...
- var module = new Module(id, parent);
- //...
- Module._cache[filename] = module;
- try {
- module.load(filename);
- } catch (err) {
- delete Module._cache[filename];
- throw err;
- }
- return module.exports;
- };
這個(gè)看似簡(jiǎn)單粗暴的處理手法,但實(shí)際上是node作者權(quán)衡各方面因素的結(jié)果。我們敬愛(ài)的npm作者issacs強(qiáng)調(diào)說(shuō)了,這不是bug,而且近期內(nèi)不會(huì)做什么改變。當(dāng)然,issacs也給出了一些規(guī)避這個(gè)陷阱的建議(具體可以參考后面給的鏈接[1])。我總結(jié)了一下,主要有兩點(diǎn):一個(gè)是在造成循環(huán)依賴的require之前把需要的東西exports出去;另一個(gè)是不要在load過(guò)程中操作未完成的模塊。
所以上面的例子的一種處理方法就是把各自的exports語(yǔ)句放到require語(yǔ)句前面,然后再運(yùn)行,可以看到打印了正確的值。
從前面的分析來(lái)看,循環(huán)依賴的陷阱出現(xiàn)的條件比較苛刻:一個(gè)是循環(huán)依賴,另一個(gè)是在load期間調(diào)用未加載完成的對(duì)象。所以大家平常不怎么會(huì)遇到。但我之前就曾華麗麗的邂逅了這個(gè)陷阱,在這里拿出來(lái)當(dāng)一下反面教材。。。
場(chǎng)景簡(jiǎn)化后大致如下:我有一堆service,每一個(gè)service負(fù)責(zé)消費(fèi)某一類消息,并且可能會(huì)產(chǎn)生新的消息給其他service消費(fèi)。從消息傳遞上來(lái)看,并沒(méi)有產(chǎn)生循環(huán)依賴。但我為了解耦,定義了一個(gè)消息中心center的角色出來(lái)進(jìn)行消息分發(fā)。center主要是維護(hù)一個(gè)type -> service的map來(lái)路由消息,這樣center就得把所有的service加載進(jìn)來(lái),于是產(chǎn)生了center->service的依賴。另一面,每個(gè)service又需要通過(guò)center來(lái)分發(fā)它們新產(chǎn)生的消息,于是又出現(xiàn)了service->center的依賴,循環(huán)依賴就這么出來(lái)了。剛好在service加載的過(guò)程中,又調(diào)用了center的一個(gè)方法,就發(fā)生了undefined的錯(cuò)誤。
這個(gè)問(wèn)題查清楚原因以后,解決起來(lái)并不困難。
一種方法就是按前面的方法,在代碼層面上規(guī)避循環(huán)依賴的陷阱;
另外也可以在設(shè)計(jì)的層面上徹底避免循環(huán)依賴的出現(xiàn)。我的場(chǎng)景之所以出現(xiàn)循環(huán)依賴,是因?yàn)閏enter和service都需要知道對(duì)方的存在,即 center <- -> service。如果采用依賴注入的方式,則可以切斷這種直接依賴,類似于center <- container -> service。即加入一個(gè)container角色,把center和service都先加載進(jìn)來(lái),然后再用IOC的方法把依賴關(guān)系建立好。這樣center和service都無(wú)須知道對(duì)方具體的文件所在了,也就不會(huì)循環(huán)的require對(duì)方了。
總的來(lái)說(shuō),循環(huán)依賴的陷阱并不大容易出現(xiàn),但一旦出現(xiàn)了,在實(shí)際的代碼中也許還真不好定位。它的存在給我們提了個(gè)醒,注意你工程中的依賴關(guān)系。哪天node對(duì)你抱怨,一個(gè)你明明已經(jīng)exports了的方法undefined,我們就該提醒一下自己:哦,也許是它來(lái)了:)
原文:http://club.cnodejs.org/topic/4f16442ccae1f4aa27001045
【編輯推薦】