咱們在寫node的時候有可能會遇到循環依賴的狀況,什麼是循環依賴,怎麼避免或解決循環依賴問題?javascript
先看一段官網給出的循環依賴的代碼:java
a.js
:node
console.log('a starting'); exports.done = false; var b = require('./b.js'); // ---> 1 console.log('in a, b.done = %j', b.done); exports.done = true; console.log('a done') // ---> 4
b.js
:c++
console.log('b starting'); exports.done = false; var a = require('./a.js'); // ---> 2 // console.log(a); ---> {done:false} console.log('in b, a.done = %j', a.done); // ---> 3 exports.done = true; console.log('b done');
main.js
:git
console.log('main starting'); var a = require('./a.js'); // --> 0 var b = require('./b.js'); console.log('in main, a.done=%j, b.done=%j', a.done, b.done);
若是咱們啓動 main.js
會出現什麼狀況? 在 a.js
中加載 b.js
,而後在b.js
中加載 a.js
,而後再在 a.js
中加載 b.js
嗎?這樣就會形成循環依賴死循環。github
讓咱們執行看看:shell
$ 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
能夠看到程序並無陷入死循環,從上面的執行結果能夠看到 main.js
中先require
了 a.js
,a.js
中執行完了console
和export.done=fasle
以後,轉而去加載b.js
,待b.js
被load完以後,再返回a.js
中執行完剩下的代碼。數組
我在官網的代碼基礎上增長了一些註釋,基本 load 順序就是按照這個0-->1-->2-->3-->4
的順序去執行的,而後在第二步下面我打印出了require('./a')
的結果,能夠看到是{done:false}
,能夠猜想在b.js
中require('./a')
的結果是a.js
中已經執行到的exports
出的值。緩存
上面所說的還只是基於結果基礎上的猜想,沒有什麼說服力,爲了驗證個人猜想是正確的,我把 Node 的源碼稍微翻看了一些,C++ 的代碼看不懂不要緊,能看懂 JS 的部分就能夠了,下面就是 Node 源碼的分析(主要是 module 的分析, Node 源碼在此):閉包
將會分析的主要源碼:
node/src/node.js
node/lib/module.js
C++ 的代碼我看不懂,總而言之,在我查了資料以後知道當咱們在shell
中輸入node main.js
以後,會先執行 node/src/node.cc
,而後會執行 node/src/node.js
, 因此C++代碼不分析,從分析 node/src/node.js
開始(只會分析和主題相關的代碼)。
node.js
文件主要結構爲
(function(process) { this.global = this function startup() { ... } startup() })
這種閉包代碼很常見,從名字能夠看出,此處爲啓動文件。接下來看看 startup 函數中有一大塊條件語句,我刪除大多數無關代碼,以下:
if (process.argv[1]) { // ... var Module = NativeModule.require('module'); Module.runMain(); }
我把無關的代碼基本都刪除了。能夠看到這段代碼主要作的事是先經過 Native 引入module
模塊,執行 Module.runMain()
。
不少人都知道 require
核心代碼,如 require('path'),不須要寫全路徑,Node 是怎樣作到的呢?
Node 採用了 V8 附帶的 js2c.py 工具,將全部內置的 JavasSript 代碼( src/node.js 和 lib/*.js) 轉成 c++ 裏面的數組生成 node_navtives.h 頭文件。
在這個過程當中, JavasSript 以字符串的形式存儲在 node 命名空間中, 是不可直接執行的。
在啓動 Node 進程時, JavaScript 代碼直接加載進內存中。Node 在啓動時,會生成一個全局變量 process, 並提供 binding() 方法來協助加載內建模塊。
上面大段介紹基本引自樸老師的「深刻淺出 Node.js」。大概理解就是在啓動命令的時候,Node 會把 node.js
和 lib/*.js
的內容都放到 process
中傳入當前閉包中,咱們在當前函數就能夠經過process.binding('natives')
取出來放到 _source 中,以下代碼所示:
function NativeModule(id) { this.filename = id + '.js'; this.id = id; this.exports = {}; this.loaded = false; } NativeModule._source = process.binding('natives'); NativeModule._cache = {};
接下來看看NativeModule.require
作了哪些事情:
NativeModule.require = function(id) { if (id == 'native_module') { return NativeModule; } var cached = NativeModule.getCached(id); if (cached) { return cached.exports; } var nativeModule = new NativeModule(id); nativeModule.cache(); nativeModule.compile(); return nativeModule.exports; };
這上面的代碼代表內建模塊被緩存,就直接返回內建模塊的exports
,若是沒有的話,就生成一個核心模塊的實例,而後先把模塊根據id來cache
,而後調用nativeModule.compile
接口編譯源文件:
NativeModule.getSource = function(id) { return NativeModule._source[id]; }; NativeModule.wrap = function(script) { return NativeModule.wrapper[0] + script + NativeModule.wrapper[1]; }; NativeModule.wrapper = [ '(function (exports, require, module, __filename, __dirname) {\n', '\n});' ]; NativeModule.prototype.compile = function() { var source = NativeModule.getSource(this.id); source = NativeModule.wrap(source); var fn = runInThisContext(source, { filename: this.filename, lineOffset: -1 }); fn(this.exports, NativeModule.require, this, this.filename); this.loaded = true; }; NativeModule.prototype.cache = function() { NativeModule._cache[this.id] = this; };
cache 是把實例根據 id 放到 _cache 對象中。先從 _source 中取出對應id的源文件字符串,包上一層(function (exports, require, module, __filename, __dirname) {\n','\n});
。好比main.js
最終變成以下JS代碼的字符串:
(function (exports, require, module, __filename, __dirname) { // 若是是main.js console.log('main starting'); var a = require('./a.js'); // --> 0 var b = require('./b.js'); console.log('in main, a.done=%j, b.done=%j', a.done, b.done); })
runInThisContext
是將被包裝後的源字符串轉成可執行函數,(runInThisContext
來自contextify
模塊),runInThisContext
的做用,相似eval
,再執行這個被eval
後的函數,就算被 load 完成了,最後把 load 設爲 true。
能夠看到fn
的實參爲 this.exports; NativeModule.require; this; this.filename;
。
因此require('module')
的做用是加載/lib/module.js
文件。讓咱們再回到 startup 函數,加載完 module.js,緊接着運行 Module.runMain()
方法。(估計有人忘了前面的startup函數是幹嗎的,我再放一次,免得再拉回去了)
if (process.argv[1]) { // ... var Module = NativeModule.require('module'); Module.runMain(); }
上面走完了NatvieModule
的加載代碼。再看看module.js
是怎樣加載用戶使用的文件的。
function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; if (parent && parent.children) { parent.children.push(this); } this.filename = null; this.loaded = false; this.children = []; } module.exports = Module; Module._cache = {}; Module._pathCache = {}; Module._extensions = {}; var modulePaths = []; Module.globalPaths = []; Module.wrapper = NativeModule.wrapper; Module.wrap = NativeModule.wrap;
這是Module
的構造函數,Module.wrapper
和Module.wrap
,是由NativeModule
賦值來的,Module._cache
是個空對象,存放全部被 load 後的模塊 id。
在node.js
文件的 startup 函數中,最後一步走到Module.runMain()
:
Module.runMain = function() { // Load the main module--the command line argument. Module._load(process.argv[1], null, true); // Handle any nextTicks added in the first tick of the program process._tickCallback(); };
在runMain
方法中調用了_load
方法:
Module._load = function(request, parent, isMain) { var filename = Module._resolveFilename(request, parent); var cachedModule = Module._cache[filename]; if (cachedModule) { return cachedModule.exports; } var module = new Module(filename, parent); Module._cache[filename] = module; module.load(filename); return module.exports; };
上述代碼照例我刪除了一些不是很相關的代碼,從剩下的代碼能夠看出_load
函數的主要乾了兩件事(還有一件加載NativeModule的代碼被我刪掉了):
先判斷當前的源文件有沒有被加載過,若是 _cache 對象中存在,直接返回 _cache 中的exports對象
若是沒有被加載過,新建這個源文件的 module 的實例,並存放到 _cache 中,而後調用 load 方法。
Module.prototype.load = function(filename) { this.filename = filename; this.paths = Module._nodeModulePaths(path.dirname(filename)); var extension = path.extname(filename) || '.js'; if (!Module._extensions[extension]) extension = '.js'; Module._extensions[extension](this, filename); this.loaded = true; };
在load
方法中判斷源文件的擴展名是什麼,默認是'.js'
,(我這裏也只分析後綴是 .js
的狀況),而後調用 Module._extensions[extension]()
方法,並傳入 this 和 filename;當extension
是'.js'
的時候, 調用Module._extensions['.js']()
方法。
// Native extension for .js Module._extensions['.js'] = function(module, filename) { var content = fs.readFileSync(filename, 'utf8'); module._compile(internalModule.stripBOM(content), filename); };
這個方法是讀到源文件的字符串後,調用module._compile
方法。
Module.prototype._compile = function(content, filename) { var self = this; function require(path) { return self.require(path); } var dirname = path.dirname(filename); // create wrapper function var wrapper = Module.wrap(content); var compiledWrapper = runInThisContext(wrapper, { filename: filename, lineOffset: -1 }); var args = [self.exports, require, self, filename, dirname]; return compiledWrapper.apply(self.exports, args); };
其實跟NativeModule
的_complie
作的事情差很少。先把源文件content
包裝一層(function (exports, require, module, __filename, __dirname) {\n','\n});
, 而後經過 runInThisContext
把字符串轉成可執行的函數,最後把 self.exports, require, self, filename, dirname
這幾個實參傳入可執行函數中。
require
方法爲:
Module.prototype.require = function(path) { return Module._load(path, this); };
所謂的循環依賴就是在兩個不一樣的文件中互相應用了對方。假設按照最上面官網給出的例子中,
在 main.js
中:
require('./a.js')
;此時會調用 self.require()
,
而後會走到module._load
,在_load
中會判斷./a.js
是否被load過,固然運行到這裏,./a.js
還沒被 load 過,因此會走完整個load流程,直到_compile
。
運行./a.js
,運行到 exports.done = false
的時候,給 esports 增長了一個屬性。此時的 exports={done: false}
。
運行require('./b.js')
,同 第 1 步。
運行./b.js
,到require('./a.js')
。此時走到_load
函數的時候發現./a.js
已經被load過了,因此會直接從_cache
中返回。因此此時./a.js
尚未運行完,exports = {done.false}
,那麼返回的結果就是 in b, a.done = false
;
./b.js
所有運行完畢,回到./a.js
中,繼續向下運行,此時的./b.js
的 exports={done:true}
, 結果天然是in main, a.done=true, b.done=true
雖然Node.js
經過 cache 解決無限循環引用的問題,可是沒有解決循環引用時已加載了模塊,而exports
沒有輸出想要的值得問題,據說ES6
的import
已經完美解決這類問題,因此立個死亡 Flag,等我研究完 import ,再寫篇文章分析 import 是怎麼解決這個問題的。 爲何是死亡 Flag 呢?每一個等我 XXX 的時候,我就 OOO 的事情,最後必定不會作。^_^。
原文地址: GitHub
喜歡的點個推薦吧