module 在 nodejs 裏是一個很是核心的內容,本文經過結合 nodejs 的源碼簡單介紹 nodejs 中模塊的加載方式和緩存機制。若是有理解錯誤的地方,請及時提醒糾正。javascript
ppt 地址:http://47.93.21.106/sharing/m...html
提到 nodejs 中的模塊,就不能不提到 CommonJS。大部分人應該都知道 nodejs 的模塊規範是基於 CommonJS 的,但其實 CommonJS 不只僅定義了關於模塊的規範,完整的規範在這裏:CommonJS。內容很少,感興趣的同窗能夠瀏覽一下。固然重點是在 模塊 這一章,若是仔細讀一下 CommonJS 中關於模塊的規定,能夠發現和 node 中的模塊使用是很是吻合的。java
CommonJS 中關於模塊的規定主要有三點:node
Requiregit
模塊引入的方式和行爲,涉及到經常使用的 `require()`。
Module Contextgithub
模塊的上下文環境,涉及到 `module` 和 `exports`。
Module Identifiers面試
模塊的標識,主要用於模塊的引入
在 node.js 裏使用模塊的方式很簡單,通常咱們都是這麼用的:json
// format.js const moment = require('moment'); /* 格式化時間戳 */ exports.formatDate = function (timestamp) { return moment(timestamp).format('YYYY-MM-DD HH:mm:ss'); }
上面是一個 format.js 文件,內容比較簡單,引入了 moment 模塊,並導出了一個格式化時間的方法供其餘模塊使用。bootstrap
可是你們有沒有考慮過,這裏的 require
和 exports
是在哪裏定義的,爲何咱們能夠直接拿來使用呢?api
實際上,nodejs 加載文件的時候,會在文件頭尾分別添加一段代碼:
頭部添加
(function (exports, require, module, filename, dirname) {
尾部添加
});
最後處理成了一個函數,而後才進行模塊的加載:
(function (exports, require, module, __filename, __dirname) { // format.js const moment = require('moment'); /* 格式化時間戳 */ exports.formatDate = function (timestamp) { return moment(timestamp).format('YYYY-MM-DD HH:mm:ss'); } });
因此 exports
, require
, module
其實都是在調用這個函數的時候傳進來的了。
這裏還有兩個比較細微的點,也是在不少面試題裏面會出現的
經過 var
、let
、const
定義的變量變成了局部變量;沒有經過關鍵字聲明的變量會泄露到全局
exports
是一個形參,改變 exports
的引用不會起做用
第一點是做用域的問題,第二點能夠問到 js 的參數傳遞是值傳遞仍是引用傳遞。
固然,若是隻是這樣講,好像只是個人一面之詞,怎麼證實 nodejs 確實是這樣包裝的呢,這裏能夠用兩個例子來證實:
➜ echo 'dvaduke' > bad.js ➜ node bad.js /Users/sunhengzhe/Documents/learn/node/modules/demos/bad.js:1 (function (exports, require, module, __filename, __dirname) { dvaduke ^ ReferenceError: dvaduke is not defined at Object.<anonymous> (/Users/sunhengzhe/Documents/learn/node/modules/demos/bad.js:1:63) at Module._compile (module.js:569:30) at Object.Module._extensions..js (module.js:580:10) at Module.load (module.js:503:32) at tryModuleLoad (module.js:466:12) at Function.Module._load (module.js:458:3) at Function.Module.runMain (module.js:605:10) at startup (bootstrap_node.js:158:16) at bootstrap_node.js:575:3
我在 bad.js 裏面隨便輸入了一個單詞,而後運行這個文件,能夠看到運行結果會拋出異常。在異常信息裏面咱們會驚訝地發現 node 把那行函數頭給打印出來了,而在 bad.js 裏面是隻有那個單詞的。
➜ echo 'console.log(arguments)' > arguments.js ➜ node arguments.js { '0': {}, '1': { [Function: require] resolve: [Function: resolve], main: Module { id: '.', exports: {}, parent: null, filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/arguments.js', loaded: false, children: [], paths: [Array] }, extensions: { '.js': [Function], '.json': [Function], '.node': [Function] }, cache: { '/Users/sunhengzhe/Documents/learn/node/modules/demos/arguments.js': [Object] } }, '2': Module { id: '.', exports: {}, parent: null, filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/arguments.js', loaded: false, children: [], paths: [ '/Users/sunhengzhe/Documents/learn/node/modules/demos/node_modules', '/Users/sunhengzhe/Documents/learn/node/modules/node_modules', '/Users/sunhengzhe/Documents/learn/node/node_modules', '/Users/sunhengzhe/Documents/learn/node_modules', '/Users/sunhengzhe/Documents/node_modules', '/Users/sunhengzhe/node_modules', '/Users/node_modules', '/node_modules' ] }, '3': '/Users/sunhengzhe/Documents/learn/node/modules/demos/arguments.js', '4': '/Users/sunhengzhe/Documents/learn/node/modules/demos' }
在 arguments.js 這個文件裏打印出 argumens 這個參數,咱們知道 arguments 是函數的參數,那麼打印結果能夠很好的說明 node 往函數裏傳入了什麼參數:第一個是 exports
,如今固然是空,第二個是 require
,是一個函數,第三個是 module
對象,還有兩個分別是 __filename
和 __dirname
發現這個地方以後我相信你們都會對 nodejs 的源碼感興趣,而 nodejs 自己是開源的,咱們能夠在 github 上找到 nodejs 的源碼:node
實際上包裝模塊的代碼就在 /lib/module.js
裏面:
Module.prototype._compile = function(content, filename) { content = internalModule.stripShebang(content); // create wrapper function var wrapper = Module.wrap(content); var compiledWrapper = vm.runInThisContext(wrapper, { filename: filename, lineOffset: 0, displayErrors: true }); // ... }
_compile
函數是編譯 nodejs 文件會執行的方法,函數中的 content
就是咱們文件中的內容,能夠看到調用了一個 Module.wrap 方法,那麼 Module.wrap 作了什麼呢?這裏須要找到另外一個文件,包含內置模塊定義的 /lib/internal/bootstrap_node.js
,裏面有對 wrap 的操做:
NativeModule.wrap = function(script) { return NativeModule.wrapper[0] + script + NativeModule.wrapper[1]; }; NativeModule.wrapper = [ '(function (exports, require, module, __filename, __dirname) { ', '\n});' ];
確實是前面說到的,添加函數頭尾的內容。
其實知道這個處理以後,咱們能夠開一些奇怪的腦洞,好比寫一段好像會報錯的文件:
// inject.js }); (function () { console.log('amazing');
這個文件看起來沒頭沒尾,可是通過 nodejs 的包裝後,是能夠運行的,會打印出 amazing
,看起來頗有意思。
上面只是帶你們看了一下 module.js 裏的一小段代碼,實際上若是要搞明白 nodejs 模塊運做的機制,有三個文件是比較核心的:
/lib/module.js
加載非內置模塊
/lib/internal/module.js
提供一些相關方法
/lib/internal/bootstrap_node
定義了加載內置模塊的 NativeModule,同時這也是 node 的入口文件
咱們知道 node 的底層是 C 語言編寫的,node 運行是,會調用 node.cc 這個文件,而後會調用 bootstrap_node 文件,在 bootstrap_node 中,會有一個 NativeModule 來加載 node 的內置模塊,包括 module.js,而後經過 module.js 加載非內置模塊,好比用戶自定義的模塊。(因此說模塊是多麼基礎)
調用關係以下:
下面重點介紹一下 module。在 nodejs 裏面,一般一個文件就表明了一個模塊,而 module 這個對象就表明了當前這個模塊。咱們能夠嘗試打印一下 module:
echo "console.log(module)" > print-module.js node print-module.js
打印結果以下:
Module { id: '.', exports: {}, parent: null, filename: '/Users/sunhengzhe/Documents/learn/node/modules/demo-1.js', loaded: false, children: [], paths: [ '/Users/sunhengzhe/Documents/learn/node/modules/node_modules', '/Users/sunhengzhe/Documents/learn/node/node_modules', '/Users/sunhengzhe/Documents/learn/node_modules', '/Users/sunhengzhe/Documents/node_modules', '/Users/sunhengzhe/node_modules', '/Users/node_modules', '/node_modules' ] }
能夠看到 module 這個對象有不少屬性,exports 咱們先不說了,它就是這個模塊須要導出的內容。filename 也不說了,文件的路徑名。paths 很明顯,是當前文件一直到根路徑的全部 node_modules 路徑,查找第三方模塊時會用到它。咱們下面介紹一下 id、parent、children 和 loaded。
在 nodejs 裏面,模塊的 id 分兩種狀況,一種是當這個模塊是入口文件時,此時模塊的 id 爲 .
,另外一種當模塊不是入口文件時,此時模塊的 id 爲模塊的文件路徑。
舉個例子,當文件是入口文件時:
➜ echo 'console.log(module.id)' > demo-1-single-file.js ➜ node demo-1-single-file.js .
此時 id 爲 .
當文件不是入口文件時:
➜ cat demo-2-require-other-file.js const other = require('./demo-1-single-file'); console.log('self id:', module.id); ➜ node demo-2-require-other-file.js /Users/sunhengzhe/Documents/learn/node/modules/demos/demo-1-module-id/demo-1-single-file.js self id: .
運行 demo-2-require-other-file.js,首先打印出 demo-1-single-file 的內容,能夠發現此時 demo-1-single-file 的 id 是它的文件名:由於它如今不是入口文件了。而做爲入口文件的 demo-2-require-other-file.js 的 id 變成了 .
這兩個含義很明確,是模塊的調用方和被調用方。
若是咱們直接打印一個入口文件的 module,結果以下:
➜ echo 'console.log(module)' > demo-1-single-file.js ➜ node demo-1-single-file.js Module { id: '.', exports: {}, parent: null, filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-1-single-file.js', loaded: false, children: [], paths: [...] }
篇幅限制,就不顯示 paths 了。能夠看到 parent 爲 null:由於沒有人調用它;children 爲空:由於它沒有調用別的模塊。那麼咱們再新建一個文件引用一下這個模塊:
➜ cat demo-2-require-other-file.js require('./demo-1-single-file'); console.log(module); ➜ node demo-2-require-other-file.js Module { id: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-1-single-file.js', exports: {}, parent: Module { id: '.', exports: {}, parent: null, filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-2-require-other-file.js', loaded: false, children: [ [Circular] ], paths: [...] }, filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-1-single-file.js', loaded: false, children: [], paths: [...] } ------------------------ Module { id: '.', exports: {}, parent: null, filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-2-require-other-file.js', loaded: false, children: [ Module { id: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-1-single-file.js', exports: {}, parent: [Circular], filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-1-single-file.js', loaded: true, children: [], paths: [Array] } ], paths: [...] }
上面輸出了兩個 module,爲了方便閱讀,我用分割線分隔了一下。第一個 module 是 demo-1-single-file 打印出來的,它的 parent 如今有值了,由於 demo-2-require-other-file.js 引用它了。它的 children 依舊是空,畢竟它沒有引用別人。
而 demo-2-require-other-file.js 的 parent 爲 null,children 有值了,能夠看到就是 demo-1-single-file。
注意裏面還出現了 [Circular],由於 demo-1-single-file 的 parent 的 children 就是它本身,爲了防止循環輸出,nodejs 在這裏省略掉了,應該很好理解。
loaded 從字面意思上也好理解,表明這個模塊是否已經加載完了。但咱們會發如今上面的全部輸出中,loaded 都是 false。
➜ cat demo-1-print-sync.js console.log(module.loaded); ➜ node demo-1-print-sync.js false
咱們能夠在 node 的下一個 tick 裏面去輸出,就能獲得正確的 loaded 的值了:
➜ cat demo-2-print-next-tick.js setImmediate(function () { console.log(module.loaded); }); ➜ node demo-2-print-next-tick.js true
模塊究竟是如何加載的?在 /lib/module.js
裏,能夠找到模塊加載的函數 _load
,這裏 node 的註釋很好地描述了加載的次序:
// Check the cache for the requested file. // 1. If a module already exists in the cache: return its exports object. // 2. If the module is native: call `NativeModule.require()` with the // filename and return the result. // 3. Otherwise, create a new module for the file and save it to the cache. // Then have it load the file contents before returning its exports // object. Module._load = function(request, parent, isMain) { //...
翻譯一下,大概就是這個流程:
有緩存(二次加載)
直接讀取緩存內容
無緩存(首次加載或清空緩存以後)
路徑分析
文件定位
編譯執行
首先看一下無緩存的狀況。nodejs 首先須要對文件進行定位,找到文件才能進行加載,其實全部的細節都隱藏在了 require
方法裏面,咱們調用 require,nodejs 返回模塊對象,那麼 require 是怎麼找到咱們須要的模塊的呢?
簡單來說,大體是:
嘗試加載核心模塊
嘗試以文件形式加載
X
X.js
X.json
X.node
嘗試做爲目錄查找,尋找 package.json 文件,嘗試加載 main 字段指定的文件
嘗試做爲目錄查找,尋找 index.js、index.json、index.node
嘗試做爲第三方模塊進行加載
拋出異常
這裏涉及的代碼細節比較複雜,建議先直接閱讀 nodejs 的官方文檔,文檔對定位的順序描述的很是詳細:https://nodejs.org/dist/latest-v8.x/docs/api/modules.html#modules_all_together
若是有緩存的話,會直接返回緩存內容。好比這裏有個文件,內容就是打印一行星號:
➜ cat print.js console.log('********');
若是咱們在另外一個文件裏引入這個文件兩次,那麼會輸出兩行星號嗎?
➜ demo-5-cache cat demo-1-just-print-multiply.js require('./print'); require('./print'); ➜ demo-5-cache node demo-1-just-print-multiply.js ********
答案是不會的,由於第一次 require 後,nodejs 會把文件緩存起來,第二次 require 直接取得緩存的內容,參考 /lib/module.js
中的代碼:
Module._load = function(request, parent, isMain) { var cachedModule = Module._cache[filename]; if (cachedModule) { // 更新 parent 的 children updateChildren(parent, cachedModule, true); return cachedModule.exports; } //... Module._cache[filename] = module; tryModuleLoad(module, filename); //... }
那麼,若是咱們要清空緩存,勢必須要清除 Module._cache
中的內容。然而在文件裏,咱們只能拿到 module 對象,拿不到 Module 類:
➜ cat demo-2-get-Module.js console.log(Module) ➜ node demo-2-get-Module.js /Users/sunhengzhe/Documents/learn/node/modules/demos/demo-5-cache/demo-2-get-Module.js:1 (function (exports, require, module, __filename, __dirname) { console.log(Module) ^ ReferenceError: Module is not defined at Object.<anonymous> (/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-5-cache/demo-2-get-Module.js:1:75) at Module._compile (module.js:569:30) at Object.Module._extensions..js (module.js:580:10) at Module.load (module.js:503:32) at tryModuleLoad (module.js:466:12) at Function.Module._load (module.js:458:3) at Function.Module.runMain (module.js:605:10) at startup (bootstrap_node.js:158:16) at bootstrap_node.js:575:3
可是是否沒有辦法去清空緩存了呢?固然是有的。這裏咱們先看 require 是怎麼來的。
以前提到,require 是經過函數參數的方式傳入模塊的,那麼咱們能夠看一下,傳入的 require 的究竟是什麼?回到 _compile
方法:
Module.prototype._compile = function(content, filename) { content = internalModule.stripShebang(content); // create wrapper function var wrapper = Module.wrap(content); var compiledWrapper = vm.runInThisContext(wrapper, { filename: filename, lineOffset: 0, displayErrors: true }); // ... var require = internalModule.makeRequireFunction(this); result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname); // ... return result; }
簡化後的代碼如上,函數內容通過包裝以後生成了一個新的函數 compiledWrapper
,而後把一些參數傳了進去。咱們能夠看到 require 是從一個 makeRequireFunction
的函數中生成的。
而 makeRequireFunction
函數是在 /lib/internal/module.js
中定義的,看下代碼:
function makeRequireFunction(mod) { const Module = mod.constructor; function require(path) { try { exports.requireDepth += 1; return mod.require(path); } finally { exports.requireDepth -= 1; } } function resolve(request) { return Module._resolveFilename(request, mod); } require.resolve = resolve; require.main = process.mainModule; // Enable support to add extra extension types. require.extensions = Module._extensions; require.cache = Module._cache; return require; }
若是咱們直接打印 require,其實就和這裏面定義的 require 是同樣的:
➜ cat demo-1-require.js console.log(require.toString()); ➜ node demo-1-require.js function require(path) { try { exports.requireDepth += 1; return mod.require(path); } finally { exports.requireDepth -= 1; } }
其實這個 require 也沒有作什麼事情,又調用了 mod 的 require,而 mod 是經過 makeRequireFunction
傳進來的,傳入的是 this,因此歸根到底,require 是 module 原型上的方法,也就是 module.prototype.require,參考 /lib/module.js
中的代碼。
固然這裏咱們先不用追究 require 的實現方式,而是注意到 makeRequireFunction
中對 require 的定義,咱們能夠發現一行關於 _cache 的代碼:
function makeRequireFunction(mod) { // ... function require(path) {} require.cache = Module._cache; //.. return require; }
因此 nodejs 很貼心地,把 Module._cache
返回給咱們了,其實只要清空 require.cache 便可。而根據上面的代碼,Module._cache
是經過 filename 來做爲緩存的 key 的,因此咱們只須要清空模塊對應的文件名。
針對上面提到的例子,清空 print.js
的緩存:
require('./print'); // delete cache delete require.cache[require.resolve('./print')]; require('./print');
而後再打印一下
➜ node demo-1-just-print-multiply.js ******** ********
就是兩行星號了。
這裏用到了 require 的一個 resolve 方法,它和直接調用 require 方法比較像,都能找到模塊的絕對路徑名,但直接 require 還會加載模塊,而 require.resolve()
只會找到文件名並返回。因此這裏利用文件名將 cache 裏對應的內容刪除了。
本文介紹了一些 nodejs 中的源碼內容,在學習 nodejs 的過程當中,若是想查看 nodejs 的源碼(我以爲這是一個必備的過程),那麼就須要去調試源碼,打幾個 log 看一下是否是和你預期的一致,這裏說一下怎麼調試 nodejs 的源碼。
下載 node 源碼 git@github.com:nodejs/node.git
進入源碼目錄執行 ./configure
& make -j
上一步以後會在 ${源碼目錄}/out/Release/node
裏生成一個執行文件,將這個文件做爲你的 node 執行文件。
每次修改源碼後從新執行 make
命令。
好比修改代碼以後,運行 make
,而後這樣運行文件便可:
➜ /Users/sunhengzhe/Documents/project/source-code/node/out/Release/node demo-1-single-file.js