模块加载源码分析
配置 vscode 调试
用 vscode 打开文件夹,在里面创建两个文件:
- m.js 作为被加载的模块文件
- require-load.js 作为加载模块的文件
const obj = require('./m')
module.exports = {
foo: 123
}
打个断点:
创建 vscode 调试配置文件:
点击左边的【运行和调试】,点击【创建 launch.json 文件】,选择【Node.js】
修改配置文件:
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
// 忽略调试的文件
"skipFiles": [
// "<node_internals>/**" 是 node 内部模块
// 本例要分析源码,所以不能忽略,将其注释
// 注意:不能直接注释 skipFiles,否则 vscode 仍会采用默认值
// "<node_internals>/**"
],
// 启动程序后需要打开的文件,修改成正确的地址即可
"program": "${workspaceFolder}\\require-load.js"
}
]
}
点击绿色的按钮开始调试,代码运行到断点,顶部有调试进度工具,使用同 Chrome:
点击单步调试箭头(F11)进入 require
源码,它调用了另一个 require
方法:
require = function require(path) {
return mod.require(path);
};
Module.prototype.require
继续 F11 查看这个方法:
// Loads a module at the given file path. Returns that module's
// `exports` property.
Module.prototype.require = function(id) {
// `id` 就是模块路径(当前是`./m`)
// 先是对其进行了一些校验
validateString(id, 'id');
if (id === '') {
throw new ERR_INVALID_ARG_VALUE('id', id,
'must be a non-empty string');
}
requireDepth++;
try {
// 最终调用 `Module._load` 方法,这个方法就是 `reuqire` 的主要逻辑
// 第一个参数:模块地址
// 第二个参数:当前 Module 的实例
// 第三个参数:是否主模块
return Module._load(id, this, /* isMain */ false);
} finally {
requireDepth--;
}
};
Module._load
Module._load = function(request, parent, isMain) {
let relResolveCacheIdentifier;
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
// Fast path for (lazy loaded) modules in the same directory. The indirect
// caching is required to allow cache invalidation without changing the old
// cache key names.
relResolveCacheIdentifier = `${parent.path}\x00${request}`;
// 使用 parent 拼接缓存的 key
const filename = relativeResolveCache[relResolveCacheIdentifier];
if (filename !== undefined) {
// 从缓存中查找 - 缓存优先
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
// 如果找到,直接返回它的 exports
return cachedModule.exports;
}
delete relativeResolveCache[relResolveCacheIdentifier];
}
}
// 到这里表示根据 parent 查找缓存失败
// _resolveFilename 方法用于将模块地址转换成绝对路径
const filename = Module._resolveFilename(request, parent, isMain);
// 使用绝对路径作为 key 去查找缓存 - 缓存优先
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
const parseCachedModule = cjsParseCache.get(cachedModule);
if (parseCachedModule && !parseCachedModule.loaded)
parseCachedModule.loaded = true;
else
// 如果找到,直接返回它的 exports
return cachedModule.exports;
}
// 如果是内置的核心模块,则加载 - 缓存优先后是内置模块优先
const mod = loadNativeModule(filename, request);
// 如果加载成功,直接返回它的 exports
if (mod && mod.canBeRequiredByUsers) return mod.exports;
// Don't call updateChildren(), Module constructor already does.
// 获取缓存对象,如果没有则创建一个空的对象,用于加载模块
// Module 实例化的对象就是每个模块下的 `module` 属性,可以查看一下它的源码了解 `module` 对象的属性
const module = cachedModule || new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
// 缓存当前的 module
Module._cache[filename] = module;
if (parent !== undefined) {
relativeResolveCache[relResolveCacheIdentifier] = filename;
}
let threw = true;
try {
// Intercept exceptions that occur during the first tick and rekey them
// on error instance rather than module instance (which will immediately be
// garbage collected).
if (enableSourceMaps) {
try {
module.load(filename);
} catch (err) {
rekeySourceMap(Module._cache[filename], err);
throw err; /* node-do-not-add-exception-line */
}
} else {
// 调用通过 module 原型定义的 load 方法加载模块(可以先跳到下面查看源码,再返回这里)
// 实际上就是获取模块中的 exports(填充到 module 本身上)
module.load(filename);
}
threw = false;
} finally {
// ...
}
// 最终返回/导出 exports (当前就是 {foo: 123} )
// 到这里基本上 require('./m') 就完成了
return module.exports;
};
Module 构造函数
这个构造函数创建的实例就是每个模块下的 module
属性。
function Module(id = '', parent) {
this.id = id;
this.path = path.dirname(id);
this.exports = {};
this.parent = parent;
updateChildren(parent, this, false);
this.filename = null;
this.loaded = false;
this.children = [];
}
Module.prototype.load
// Given a file name, pass it to the proper extension handler.
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));
// 获取文件扩展名(当前是 `.js`)
const extension = findLongestRegisteredExtension(filename);
// allow .mjs to be overridden
if (filename.endsWith('.mjs') && !Module._extensions['.mjs']) {
throw new ERR_REQUIRE_ESM(filename);
}
// 调用相应的文件处理函数进行解析(可以优先跳到下面查看源码,再返回这里)
// _extensions 包含三个方法 { .js: f, .json(): f, .node: f }
Module._extensions[extension](this, filename);
// 经过解析(当前就是 js 模块内容被执行并填充到模块实例中)
// 变更加载状态
this.loaded = true;
// ...
};
Module._extensions['.js']
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
if (filename.endsWith('.js')) {
const pkg = readPackageScope(filename);
// Function require shouldn't be used in ES modules.
if (pkg && pkg.data && pkg.data.type === 'module') {
const parentPath = module.parent && module.parent.filename;
const packageJsonPath = path.resolve(pkg.path, 'package.json');
throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath);
}
}
// If already analyzed the source, then it will be cached.
// 从懒加载的缓存中查找资源
const cached = cjsParseCache.get(module);
// content 用于存储模块文件(当前是 m.js)中读取的内容(文本字符串,不是可运行的代码)
let content;
if (cached && cached.source) {
content = cached.source;
cached.source = undefined;
} else {
// 同步读取文件
content = fs.readFileSync(filename, 'utf8');
}
// 运行文件内容,提取相关变量
module._compile(content, filename);
};
module._compile
源码
该方法最终通过将文件的文本字符串包装成一个函数。由于嵌套太深,本例不去查找具体实现,可以直接跳到一些方法调用之后查看调用的结果。
// Run the file contents in the correct scope or sandbox. Expose
// the correct helper variables (require, module, exports) to
// the file.
// Returns exception, if any.
Module.prototype._compile = function(content, filename) {
let moduleURL;
let redirects;
if (manifest) {
moduleURL = pathToFileURL(filename);
redirects = manifest.getRedirector(moduleURL);
manifest.assertIntegrity(moduleURL, content);
}
maybeCacheSourceMap(filename, content, this);
// 将模块的内容包装成一个接收 `'exports , require , module , __filename , __dirname'` 参数的函数
// 因为有了这层包装,所以在 NodeJS 任意模块中都可以直接使用的这个5个参数所表示的变量
// 之后将被调用并传入相应的实参
const compiledWrapper = wrapSafe(filename, content, this);
var inspectorWrapper = null;
// ...
// 准备 compiledWrapper 方法的参数
const dirname = path.dirname(filename);
const require = makeRequireFunction(this, redirects);
let result;
const exports = this.exports;
const thisValue = exports;
const module = this;
if (requireDepth === 0) statCache = new Map();
if (inspectorWrapper) {
result = inspectorWrapper(compiledWrapper, thisValue, exports,
require, module, filename, dirname);
} else {
// 调用包装的函数并传递参数
// 内部利用 Node 的 vm 模块实现安全的执行,它是类似于虚拟机的沙箱环境
// 这里将 this 指向了 thisValue 实际上就是模块实例的 exports
// 这也就是为什么在 Nodejs 模块中使用 this 时它指向的是一个空对象,而不是 global 的原因
result = compiledWrapper.call(thisValue, exports, require, module,
filename, dirname);
}
hasLoadedAnyUserCJSModule = true;
if (requireDepth === 0) statCache = null;
return result;
};
包装的方法
这里将模块内容包装成了一个方法,接收 5 个参数,之后再被调用并传递这 5 个参数。
所以这 5 个参数就是所有模块中都可以直接使用的变量。
包装的原理大致是在模块内容前后拼接函数声明字符串,就像:
var content = `\nmodule.exports = {\r\n foo: 123\r\n}\r\n\n`
var packFunction = 'function(exports,require,module,__filename,__dirname){' + content + '}'
然后在 vm 创建的沙箱环境中将这个字符串作为 JS 运行,获取这个函数。
效果类似:
var content = `\nmodule.exports = {\r\n foo: 123\r\n}\r\n\n`
var compiledWrapper = new Function('exports , require , module , __filename , __dirname', content)
关于 this 指向
在调用包装的方法并传递参数的时候绑定了 this
为 this.exports
,它的初始值就是一个空对象,所以在模块中定义 exports
内容之前,this
都指向一个空对象。
// 准备 compiledWrapper 方法的参数
const dirname = path.dirname(filename);
const require = makeRequireFunction(this, redirects);
let result;
const exports = this.exports;
const thisValue = exports;
const module = this;
// 这里将 this 指向了 thisValue 实际上就是模块实例的 exports
// 这也就是为什么在 Nodejs 模块中使用 this 时它指向的是一个空对象,而不是 global 的原因
result = compiledWrapper.call(thisValue, exports, require, module,
filename, dirname);
模块示例:
console.log(this) // {}
// 修改 exports 对象的属性,并没有改变 exports 的引用
exports.foo = 123
console.log(this) // { foo: 123}
但是如果直接用 module.exports = {}
修改 exports
,因为修改的是它的引用,所以 this
指向的对象仍是原来的 exports
地址:
console.log(this) // {}
// module.exports 的引用地址变更,已经不再指向原来的 {}
module.exports = {
foo: 123
}
// this 仍指向原来的 {}
console.log(this) // {}