模塊原理
在開始 Node 原理剖析之前,想講一個十分簡單的題目:module.exports 和 exports 有什么區(qū)別?這道題也算是面試必問題目之一了,答不出來面試基本就涼涼了。這里為尚未了解到的猿同胞們再解釋一遍。
1.exports 是 module.exports 的一個引用,即 exports = module.exports = {},二者所存的地址變量指向同一個對象。
2.暴露其實只會暴露出 module.exports 指向的對象,所以如果 exports 所存的地址指向另一個對象,則無法暴露,這也是為什么不能直接給 exports 賦值(exports = xxx)。
再深問一點:為什么暴露的是 module.exports?可能已經(jīng)有猿同胞了解過。
1.Node 執(zhí)行每個 js 文件時,其實是把內(nèi)容包裹在一個函數(shù)中,然后執(zhí)行這個函數(shù)。
2.這個函數(shù)傳入的參數(shù)共有 exports、require、module、__filename、__dirname,所以我們可以在 js 文件中直接使用這些參數(shù)。而其它模塊導(dǎo)入時會導(dǎo)入 module 的 exports 屬性值,而不是 exports 引用。
那繼續(xù)深挖:Node 是如何給文件包裹函數(shù)的?如何執(zhí)行這個函數(shù)?exports / module.exports 和 require 是如何實現(xiàn)的?本篇就將解答這三個問題。
論據(jù):Node 自帶許多模塊,且目前仍遵循 CommonJS 規(guī)范:
1.在 CommonJS 規(guī)范中,一個文件就是一個模塊。
2.在 CommonJS 規(guī)范中,exports 暴露模塊數(shù)據(jù),require 導(dǎo)入模塊數(shù)據(jù)。
3.Node 模塊中,fs 文件模塊可以讀取文件為二進(jìn)制或字符串。(顯然二進(jìn)制或字符串都無法執(zhí)行)
4.Node 模塊中,vm 模塊具有安全虛擬機(jī)環(huán)境可以將字符串轉(zhuǎn)化為代碼執(zhí)行。
結(jié)論:require 導(dǎo)入模塊數(shù)據(jù) + 文件 = 模塊 => require 讀取文件。因此,在 require 函數(shù)中,本質(zhì)是根據(jù)傳入的路徑參數(shù),用 fs 模塊讀取相應(yīng)的文件為字符串,在字符串前后拼接被轉(zhuǎn)化成字符串的函數(shù),用 vm 虛擬機(jī)執(zhí)行函數(shù)。
在使用 vm 虛擬機(jī)之前,補(bǔ)充一個知識點,runInThisContext 和 runInNewContext 的區(qū)別。
引入 vm 模塊,通過 vm 的 runInThisContext 方法執(zhí)行字符串,該字符串必須可以轉(zhuǎn)化為代碼執(zhí)行。


可見是可以執(zhí)行的,那么可不可以植入變量,尤其是虛擬機(jī)所處文件的變量。


報錯 str 未定義,可見虛擬機(jī)外部變量是無法訪問的,但我們知道,node 是存在全局變量的,虛擬機(jī)和虛擬機(jī)所在文件共處一個 node 環(huán)境,那么全局變量能否植入。


又正常輸出了,那如何將虛擬機(jī)和虛擬機(jī)所在文件所處環(huán)境分開,那就需要使用 runInNewContext。


再次報錯,可見 runInNewContext 無法訪問 global 中的變量。
回到正題,繼續(xù)看 Node 如何實現(xiàn) require,首先肯定需要定義一個 require 函數(shù),忽略內(nèi)部的 try finally,require 函數(shù)體僅剩一句,self 就代表執(zhí)行 require 的模塊,相當(dāng)于執(zhí)行每個模塊自身的 require 方法。
// 原版
function require(path) {
try {
exports.requireDepth += 1;
return self.require(path);
} finally {
exports.requireDepth -= 1;
}
}
// 忽略try finally
function require(path) {
return self.require(path);
}
而模塊自身的 require 方法所在文件引入了 assert 斷言庫,斷言庫內(nèi)容我在《遺落之城——Unit Test》中有寫過。這里斷言 path 必須存在且必須是 string 類型,成功后調(diào)用模塊的_load 靜態(tài)方法,而_load 本質(zhì)其實是加載已被加載過的模塊,并不是真正加載模塊的函數(shù)。
Module.prototype.require = function (path) {
assert(path, "missing path");
assert(typeof path === "string", "path must be a string");
return Module._load(path, this, /* isMain */ false);
};
先貼上_load 方法的完整代碼,再一句句分析。
1.Module._resolveFilename 是將 request 相對路徑情況轉(zhuǎn)為絕對路徑,如果 request 是絕對路徑就原樣返回,還有一種情況是 node 原生模塊,也會原樣返回,同時 filename 也會作為該模塊的唯一標(biāo)識。
2.先從 Module 的靜態(tài)屬性_cache(緩存)上查找,如果已有緩存,則直接返回緩存的 exports。
3.通過 NativeModule.nonInternalExists 判斷是否是原生模塊,如果是則調(diào)用原生模塊 NativeModule 的 require 方法返回該模塊的 exports。
4.兩種情況都不滿足,就說明是尚未加載的自定義模塊,會新建一個 Module 對象,將 Module 對象以 filename 為 key 放入緩存中,之后其它模塊引入相同模塊時就可以直接從緩存中取了,最后通過 tryModuleLoad 加載該模塊,并返回該模塊的 module.exports。
Module._load = function (request, parent, isMain) {
if (parent) {
debug('Module._load REQUEST %s parent: %S', request, parent.id);
}
var filename = Module._resolveFilename(request, parent, isMain);
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
}
var module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
Module._cache[filename] = module;
tryModuleLoad(module, filename);
return module.exports;
}
Module 對象要注意的點并不多,將 id 初始化為絕對路徑,將 exports 初始化為空對象,還添加了一個和 loaded 屬性,初值為 false,這個屬性值會在 tryModuleLoad 成功調(diào)用完成后變?yōu)?true,表示該模塊加載完成。
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 = []
}
tryModuleLoad 也只是嘗試加載,如果加載失敗,就會從 Module 的緩存中刪除該模塊,而到這里,真正的加載函數(shù) load 方法才冒出水面。
function tryModuleLoad(module, filename) {
var threw = true;
try {
module.load(filename);
threw = false;
} finally {
if (threw) {
delete Module._cache[filename]
}
}
}
同樣先貼上 load 方法的完整代碼再一句句分析。
1.先斷言該模塊未被加載,然后將絕對路徑賦值給該模塊的 filename 屬性(filename 相當(dāng)于和 id 同值)。
2.最核心的代碼其實就是通過 path.extname 獲取擴(kuò)展名,無擴(kuò)展名或 Module._extensions 上午該擴(kuò)展名對應(yīng)的方法時,都會賦值擴(kuò)展名為.js。
3.Module 的_extensions 靜態(tài)屬性是 object 對象,key 為擴(kuò)展名,值為加載對應(yīng)擴(kuò)展名模塊的方法函數(shù)。最后相當(dāng)于通過擴(kuò)展名拿到對應(yīng)函數(shù)執(zhí)行,執(zhí)行完成后,將該模塊加載完成狀態(tài) loaded 改為 true。
Module.prototype.load = function (filename) {
debug('load %j for module %j', filename, this.id);
assert(!this.loaded);
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;
}
_extensions 共有三種擴(kuò)展名及對應(yīng)的方法:.json、.js、.node。主要分析前兩種,因為.node 一般為 C/C++文件,這里就不涉及相關(guān)知識了。另一方面,.json 和.js 都調(diào)用了 internalModule.stripBOM 方法,作用是剝離 utf8 編碼特有的 BOM 文件頭,也沒必要細(xì)究。
首先是.json 文件,加載流程非常簡單,直接通過 fs 模塊讀取文件為字符串,然后通過 JSON.parse 轉(zhuǎn)為 json 對象賦值給 module.exports 函數(shù)。
Module._extensions['.json'] = function (module, filename) {
var content = fs.readFileSync(filename, 'utf8');
try {
module.exports = JSON.parse(internalModule.stripBOM(content))
} catch (err) {
err.message = filename + ':' + err.message;
throw err;
}
}
其次是.js,也是先用 fs 模塊讀取為字符串,但注意_compile 編譯完并未做賦值。
Module._extensions['.js'] = function (module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(internalModule.stripBOM(content), filename);
}
_compile 是對讀取完的 js 文件進(jìn)行編譯,表面上函數(shù)很復(fù)雜,但需要關(guān)注的地方并不多,先看 var wrapper = Module.wrap(content),fs 讀取完的字符串作為參數(shù)傳給 wrap 函數(shù)。
Module.prototype._compile = function (content, filename) {
var contLen = content.length;
if (contLen >= 2) {
if (content.charCodeAt(0) === 35 /*#*/ &&
content.charCodeAt(1) === 33 /*!*/ ) {
if (contLen === 2) {
content = '';
} else {
var i = 2;
for (; i < contLen; ++i) {
var code = content.charCodeAt(i);
if (code === 10 /*\n*/ || code === 13 /*\r*/ )
break;
}
if (i === contLen)
content = '';
else {
content = content.slice(i);
}
}
}
}
var wrapper = Module.wrap(content);
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});
var inspectorWrapper = null;
if (process._debugWaitConnect && process._eval == null) {
if (!resolvedArgv) {
if (process.argv[1]) {
resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
} else {
resolvedArgv = 'repl';
}
}
if (filename === resolvedArgv) {
delete process._debugWaitConnect;
inspectorWrapper = getInspectorCallWrapper();
if (!inspectorWrapper) {
const Debug = vm.runInDebugContext('Debug');
Debug.setBreakPoint(compiledWrapper, 0, 0);
}
}
}
var dirname = path.dirname(filename);
var require = internalModule.makeRequireFunction(this);
var depth = internalModule.requireDepth;
if (depth === 0) stat.cache = new Map();
var result;
if (inspectorWrapper) {
result = inspectorWrapper(compiledWrapper, this.exports, this.exports, require, this, filename, dirname);
} else {
result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);
}
if (depth === 0) stat.cache = null;
return result;
};
wrap 函數(shù)的 script 參數(shù)接收字符串后,返回一個被轉(zhuǎn)化為字符串的函數(shù)所包裹的字符串,這樣讀取到的文件內(nèi)容就相當(dāng)于位于函數(shù)體中了?;氐絖compile 函數(shù),緊接著便調(diào)用了 vm.runInThisContext 方法,但需要注意的是,執(zhí)行結(jié)果相當(dāng)于定義了一個函數(shù),并將函數(shù)存在 compiledWrapper 變量中,而并不是調(diào)用這個函數(shù)。
NativeModule.wrap = function (script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
Module.wrapper = NativeModule.wrapper;
Module.wrap = NativeModule.wrap;
忽略掉中間冗余無需關(guān)注的部分,最后需要關(guān)注的只剩 result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname)(自定義模塊都會執(zhí)行 else 情況)。
1.通過 call 方法執(zhí)行 compiledWrapper 所存的函數(shù)。
2.將函數(shù)的 this 指向_load 函數(shù)中 new Module 創(chuàng)建對象的 exports,這也是為什么我們在文件中調(diào)用 this 會輸出暴露的數(shù)據(jù)。
3.將 exports 屬性、require 方法、module 對象自身、module 對象的 filename(同時也是 id)作為文件名、module 對象所在的文件夾路徑 dirname,共同作為函數(shù)參數(shù)傳入。
4.其中 filename 是函數(shù)最開始傳入的參數(shù),dirname 通過 load 函數(shù)的 var dirname = path.dirname(filename)語句獲取。
5.至于 require,可以看到執(zhí)行了一句 var require = internalModule.makeRequireFunction(this),而這句函數(shù)就是給模塊創(chuàng)建一個 require 方法,也就是開始的 require 函數(shù)。
6.注意到,執(zhí)行完的結(jié)果 result 變量雖然 return 了但在_extensions['.js']中并沒有做賦值操作,反而是通過 call 函數(shù),將暴露的數(shù)據(jù)和 new Module 創(chuàng)建對象的 exports 綁定。
最后,再提一遍,在_load 函數(shù)中有 return module.exports,這樣 module.exports 所存的地址層層返回給 require 結(jié)果,就是返回到調(diào)用 require 的模塊中,這樣調(diào)用 require 的模塊也就拿到了被導(dǎo)入模塊所暴露的數(shù)據(jù)。