在平常開發中,當我們需要用一個模組的時候,只需要require一下就行了,但是對於其內部的原理,不一定都清楚。讀《深入淺出的nodejs》的時候,我們會發現,書中提到,每一個模組在編譯過程中,node都會在模組外面封裝一層,(function(exports, require, module, __filename, __dirname){})。但是真正呼叫require的時候,發生了什麼,必須得細究一下。
require大致過程
- require方法
Module.prototype.require = function(id) {
if (typeof id !== 'string') {
throw new ERR_INVALID_ARG_TYPE('id', 'string', id);
}
if (id === '') {
throw new ERR_INVALID_ARG_VALUE('id', id,'must be a non-empty string');
}
return Module._load(id, this, /* isMain */ false);
};
複製程式碼
- _load方法
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) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
}
// Don't call updateChildren(), Module constructor already does.
var module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
Module._cache[filename] = module;
tryModuleLoad(module, filename);
return module.exports;
};
複製程式碼
從上述兩個函式中,require的大致過程如下:
1、先檢測傳入的id是否有效。 2、如果有效,則呼叫Module._load方法,該方法主要負責載入新模組和管理模組的快取,而require本身就是對該方法的一個封裝。 3、然後會呼叫Module._resolveFilename去取檔案地址。 4、判斷是否有快取模組,如果有返回快取模組的exports。 5、如果沒有快取,在檢測檔名是否是核心模組,如果是呼叫核心模組的require。 6、如果不是核心模組,那麼,建立新的一個module物件。 7、在 Module._cache中快取該物件, 8、返回模組本身的exports物件。
上述的解讀中,我們拋開了兩個沒有談,一個是Module._resolveFilename()方法,還有一個是tryModuleLoad()方法。
filename的獲取
Module._resolveFilename = function(request, parent, isMain, options) {
if (NativeModule.nonInternalExists(request)) {
return request;
}
var paths;
if (typeof options === 'object' && options !== null &&
Array.isArray(options.paths)) {
const fakeParent = new Module('', null);
paths = [];
for (var i = 0; i < options.paths.length; i++) {
const path = options.paths[i];
fakeParent.paths = Module._nodeModulePaths(path);
const lookupPaths = Module._resolveLookupPaths(request, fakeParent, true);
if (!paths.includes(path))
paths.push(path);
for (var j = 0; j < lookupPaths.length; j++) {
if (!paths.includes(lookupPaths[j]))
paths.push(lookupPaths[j]);
}
}
} else {
paths = Module._resolveLookupPaths(request, parent, true);
}
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;
};
複製程式碼
_resolveFilename的大致流程:
1、查詢檔名是否是核心模組,如果是直接返回傳入的id 2、因為option沒有引數傳入,所以會呼叫 Module._resolveLookupPaths方法去獲取路徑 3、呼叫Module._findPath方法
我們可以寫如下測試程式碼:
console.log(require('module')._resolveFilename('./lodash'));
let paths = require('module')._resolveLookupPaths('./lodash');
console.log(paths);
console.log(require('module')._findPath("./lodash", paths[1]));
複製程式碼
輸出的結果:
/Users/laihuamin/Documents/learn-record/node_modules/lodash/lodash.js
[ './lodash',
[ '.',
'/Users/laihuamin/Documents/learn-record/node_modules',
'/Users/laihuamin/Documents/node_modules',
'/Users/laihuamin/node_modules',
'/Users/node_modules',
'/node_modules',
'/Users/laihuamin/.node_modules',
'/Users/laihuamin/.node_libraries',
'/Users/laihuamin/.nvm/versions/node/v6.9.1/lib/node' ] ]
/Users/laihuamin/Documents/learn-record/node_modules/lodash/lodash.js
複製程式碼
_resolveLookupPaths:其實就是node解析模組中的路徑查詢,他會向父目錄查詢,直到根目錄為止。 _findPath:其實就是將_resolveLookupPaths查詢出來的檔名和檔案id向匹配,返回一個檔案地址。
tryModuleLoad
function tryModuleLoad(module, filename) {
var threw = true;
try {
module.load(filename);
threw = false;
} finally {
if (threw) {
delete Module._cache[filename];
}
}
}
複製程式碼
在拿到檔案地址之後,module會呼叫這個方法取tryModuleLoad,去嘗試載入模組,如果報錯,那麼清除模組的快取。