關於Node中Module這裏不多說了(node把每一個文件都看成一個Module 的實例),只看整個分析流程
本文這只是一種情況,如果是引用node內部模塊則是有些不同的流程
require開始
//E:\\SVNPRO\node_modules\***\bin\proxy\index.js
require('util/nodeVersion.js')
這裏執行之後會走到
function require(path) {
try {
exports.requireDepth += 1;
return self.require(path);
} finally {
exports.requireDepth -= 1;
}
}
requireDepth指的是引用的深度,重點是self.require(path)
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);
};
以上全部的path在該例子中值都是‘util/nodeVersion.js’,到了Module._load(path, this, /* isMain */ false)
重點來了,這個函數有點長
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;
};
這裏的request是‘util/nodeVersion.js’,parent是指最開始執行 require('util/nodeVersion.js')
代碼的文件,就是index.js,也是一個module(模塊),parent屬性的值是
順便說一下Module,每一個module實例都有上圖這些屬性
children 數組:是該module require了哪些模塊
exports:就是該文件(模塊)開放給外部調用接口,module.exports=somethingToexports
中的somethingToexports
loaded:是否已經被加載過,加載過的模塊會添加到Module._cache中
parent:require過該模塊的模塊
paths:指所有有可能包含該模塊的路徑,通常是從當前目錄到根目錄每一級目錄下的node_modules文件夾
resolveFilenameCache:指的是index.js中前面require過的已經引入的模塊,每引入一個模塊都會把它加到這個屬性中,後面會看到
var filename = Module._resolveFilename(request, parent, isMain);
這一句是根據模塊名和parent來獲取到該模塊的絕對地址
新開一個分支來看 Module._resolveFilename (sea-node.js)
//sea-node.js
Module._resolveFilename = function(request, parent) {
var res;
//request = request.replace(/\?.*$/, '') // remove timestamp etc.
//性能優化
if(parent.resolveFilenameCache){
if(parent.resolveFilenameCache[request]){
return parent.resolveFilenameCache[request]
}
}else{
parent.resolveFilenameCache = {};
}
res = _resolveFilename(request, parent);
parent.resolveFilenameCache[request] = res;
return res;
}
該函數中,先檢查parent中resolveFilenameCache中是否包含要引入的模塊(就是request參數),如果有說明之前require過,直接返回即可
如果沒有,執行_resolveFilename(request, parent);
,這個函數和上面的函數名字一樣,但是在不同文件,不是一個函數
再下一層分支看 Module._resolveFilename (module.js)
//module.js
Module._resolveFilename = function(request, parent, isMain) {
if (NativeModule.nonInternalExists(request)) {
return request;
}
var resolvedModule = Module._resolveLookupPaths(request, parent);
var id = resolvedModule[0];
var paths = resolvedModule[1];
// look up the filename first, since that's the cache key.
debug('looking for %j in %j', id, paths);
var filename = Module._findPath(request, paths, isMain);
if (!filename) {
var err = new Error("Cannot find module '" + request + "'");
err.code = 'MODULE_NOT_FOUND';
throw err;
}
return filename;
};
Module._resolveLookupPaths 函數是利用parent和request來得到所有可能的存放該模塊的地址(如C盤下的.node_modules),例如本例中返回的resolvedModule值爲
數組第一個元素是request值,第二個元素是parent.paths和其他可能的存放模塊的地址的組合
Module._findPath函數做的事對paths數組進行遍歷,並對可能的文件(.js .json .node)後綴都進行測試,直到找到這個文件,返回的filename是該模塊的絕對路徑
之後這個函數運行結束,跳出來到上一個_resolveFilename(sea-node.js)中只剩最後一句parent.resolveFilenameCache[request] = res;
,這是之前說過的,找到該模塊後就把它添加到parent的resolveFilenameCache中,說明已經引用過。
這樣再上一層到 _load 函數中,下面是按照順序尋找該模塊:
1. Module._cache:緩存中
2. NativeModule:Node的“土著”模塊(自帶的,例如http,fs等)
3. 上面兩個都沒有,再新建模塊 var module = new Module(filename, parent);
,這時候module.exports裏面還啥都沒有,還並沒有讀取文件內容。
Module._cache[filename] = module;
這一句代碼則是將該模塊加到緩存中去,以便下次可以直接從緩存中獲取。
重點來了!!!
tryModuleLoad(module, filename);
這個函數包含了讀取該模塊文件內容;執行該函數會進入Module.load()
中
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;
};
通過path.extname()
獲取該模塊文件的擴展名,Module._extesions是一個Object,包含對應每一個後綴文件的處理函數(.js .json .node 三種),這裏是js文件所以我們直接進入該函數看
Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(internalModule.stripBOM(content), filename);
};
先讀取文件內容,然後調用module._compile處理content
Module.prototype._compile = function(content, filename) {
// Remove shebang
var contLen = content.length;
if (contLen >= 2) {
if (content.charCodeAt(0) === 35/*#*/ &&
content.charCodeAt(1) === 33/*!*/) {
if (contLen === 2) {
// Exact match
content = '';
} else {
// Find end of shebang line and slice it off
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 {
// Note that this actually includes the newline character(s) in the
// new output. This duplicates the behavior of the regular expression
// that was previously used to replace the shebang line
content = content.slice(i);
}
}
}
}
// create wrapper function
var wrapper = Module.wrap(content);
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});
var dirname = path.dirname(filename);
var require = internalModule.makeRequireFunction.call(this);
var args = [this.exports, require, this, filename, dirname];
var depth = internalModule.requireDepth;
if (depth === 0) stat.cache = new Map();
var result = compiledWrapper.apply(this.exports, args);
if (depth === 0) stat.cache = null;
return result;
};
函數比較長,只複製了一部分,這裏主要關注wrapper,這是對讀取的js文件內容進行了一個“包裝”,因爲作爲一個模塊代碼裏面肯帶會有module.exports
,那麼怎麼把exports的內容傳出來呢,其實他做的工作就是類似於:
文件內容:module.exports={name:"bob"}
在外面包裝一層匿名函數:
(function(exports,require,module){
module.exports={name:"bob"}
})(module.exports,require,module);
這樣執行完了裏面exports的內容就傳出來了。(回到源代碼)
wrapper在content上加上頭尾之後(這時候還是字符串,需要變成可以執行的js函數,例如eval的功能),經過處理(V8引擎處理的)後得到的compiledWrapper就是js函數,可以看下它執行的代碼
var args = [this.exports, require, this, filename, dirname];
var result = compiledWrapper.apply(this.exports, args);
到這裏就結束了,接口內容已經放到module.exports中了,最後loaded變成true代表該模塊已加載過了