注: 1. 本文涉及的nodejs原始碼如無特別說明則全部基於v10.14.1
如果你對NodeJs系列感興趣,歡迎關注前端神盾局或筆者微信(w979436427)交流討論node學習心得
Nodejs 中對模組的實現
本節主要基於NodeJs原始碼,對其模組的實現做一個簡要的概述,如有錯漏,望諸君不吝指正。
當我們使用require
引入一個模組的時候,概況起來經歷了兩個步驟:路徑分析和模組載入
路徑分析
路徑分析其實就是模組查詢的過程,由_resolveFilename函式實現。
我們通過一個例子,展開說明:
const http = require('http');
const moduleA = requie('./parent/moduleA');
複製程式碼
這個例子中,我們引入兩種不同型別的模組:核心模組-http
和自定義模組moduleA
對於核心模組而言,_resolveFilename
會跳過查詢步驟,直接返回,交給下一步處理
if (NativeModule.nonInternalExists(request)) {
// 這裡的request 就是模組名稱 'http'
return request;
}
複製程式碼
而對於自定義模組而言,存在以下幾種情況(_findPath)
- 檔案模組
- 目錄模組
- 從node_modules目錄載入
- 全域性目錄載入
這些在官方文件中已經闡述的很清楚了,這裡就不再贅述。
如果模組存在,那麼_resolveFilename
會返回該模組的絕對路徑,比如/Users/xxx/Desktop/practice/node/module/parent/moduleA.js
。
載入模組
獲取到模組地址後,Node就開始著手載入模組。
首先,Node會檢視模組是否存在快取中:
// filename 即模組絕對路徑
var cachedModule = Module._cache[filename];
if (cachedModule) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
複製程式碼
存在則返回對應快取內容,不存在則進一步判斷該模組是否是核心模組:
if (NativeModule.nonInternalExists(filename)) {
return NativeModule.require(filename);
}
複製程式碼
如果模組既不存在於快取中也非核心模組,那麼Node會例項化一個全新的模組物件
function Module(id, parent){
// 通常是模組絕對路徑
this.id = id;
// 要匯出的內容
this.exports = {};
// 父級模組
this.parent = parent;
this.filename = null;
// 是否已經載入成功
this.loaded = false;
// 子模組
this.children = [];
}
var module = new Module(filename, parent);
複製程式碼
而後Node會根據路徑嘗試載入。
function tryModuleLoad(module, filename) {
var threw = true;
try {
module.load(filename);
threw = false;
} finally {
if (threw) {
delete Module._cache[filename];
}
}
}
複製程式碼
對於不同的副檔名,其載入方法也有所不同。
- .js檔案(_compile)
通過fs同步讀取檔案內容後將其包裹在指定函式中:
Module.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
複製程式碼
呼叫執行此函式:
compiledWrapper.call(this.exports, this.exports, require, this,
filename, dirname);
複製程式碼
- .json檔案
通過fs同步讀取檔案內容後,用JSON.parse
解析並返回內容
var content = fs.readFileSync(filename, 'utf8');
try {
module.exports = JSON.parse(stripBOM(content));
} catch (err) {
err.message = filename + ': ' + err.message;
throw err;
}
複製程式碼
- .node
這是用C/C++編寫的擴充套件檔案,通過dlopen()方法載入最後編譯生成的檔案。
return process.dlopen(module, path.toNamespacedPath(filename));
複製程式碼
- .mjs
這是用於處理ES6模組的擴充套件檔案,是NodeJs在v8.5.0後新增的特性。對於這類副檔名的檔案,只能使用ES6模組語法import
引入,否則將會報錯(啟用--experimental-modules
的情況下)
throw new ERR_REQUIRE_ESM(filename);
複製程式碼
如果一切順利,就會返回附加在exports物件上的內容
return module.exports;
複製程式碼
模組迴圈依賴
接下來我們來探究一下模組迴圈依賴的問題:模組1依賴模組2,模組2依賴模組1,會發生什麼?
這裡只探究commonjs的情況
為此,我們建立了兩個檔案,module-a.js和module-b.js,並讓他們相互引用:
module-a.js
console.log(' 開始載入 A 模組');
exports.a = 2;
require('./module-b.js');
exports.b = 3;
console.log('A 模組載入完畢');
複製程式碼
module-b.js
console.log(' 開始載入 B 模組');
let moduleA = require('./module-a.js');
console.log(moduleA.a,moduleA.b)
console.log('B 模組載入完畢');
複製程式碼
執行module-a.js
,可以看到控制檯輸出:
開始載入 A 模組
開始載入 B 模組
2 undefined
B 模組載入完畢
A 模組載入完畢
複製程式碼
這時因為每個require
都是同步執行的,在module-a
完全載入前需要先載入./module-b
,此時對於module-a
而言,其exports
物件上只附加了屬性a
,屬性b
是在./module-b
載入完成後才賦值的。
QA
- 如何刪除模組快取?
可以通過delete require.cache(moduleId)
來刪除對應模組的快取,其中moduleId表示的是模組的絕對路徑,一般的,如果我們需要對某些模組進行熱更新,可以使用此特性,舉個例子:
// hot-reload.js
console.log('this is hot reload module');
// index.js
const path = require('path');
const fs = require('fs');
const hotReloadId = path.join(__dirname,'./hot-reload.js');
const watcher = fs.watch(hotReloadId);
watcher.on('change',(eventType,filename)=>{
if(eventType === 'change'){
delete require.cache[hotReloadId];
require(hotReloadId);
}
});
複製程式碼
- Node中可以使用ES6 模組嗎?
從8.5.0版本開始,NodeJs開始支援原生ES6模組,啟用該功能需要兩個條件:
- 所有使用ES6模組的副檔名都必須是.mjs
- 命令列選項--experimental-modules node --experimental-modules index.mjs
node --experimental-modules index.mjs
複製程式碼
但是截止到NodeJs v10.15.0,ES6模組的支援依舊是實驗性的,筆者並不推薦在公司專案中使用
參考
- nodejs-loader.js
- 樸靈. 深入淺出Node.js