共计 9275 个字符,预计需要花费 24 分钟才能阅读完成。
1. 指标
摸索 Node.js
的 require
办法是如何实现的
2. 调试办法
2.1 点击增加配置
2.2 配置相干信息
这里须要留神的是,把 skipFiles
须要把 <node_internals>/**
正文掉,这样才可能 debug Node 的源码。
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "NodeStep", // 独自调试 js,即能够间接运行 js
"type": "node",
"request": "launch",
"program": "${file}", //
"cwd": "${workspaceRoot}",
"skipFiles": [// "<node_internals>/**"]
}
]
}
3. require 执行的过程是怎么的
在第八行打断点,之后就能够点击 debug 按钮了,咱们能够看到调用堆栈中,目前进行执行的函数。单步调试。
能够看到是调用了一个工具函数,最终调用了 mod.require
办法。持续单步调用。
下面的 mod.require
调用的是 loader.js
的 Module.prototype.require
办法,而后调用 Module._load
静态方法。持续单步调用。
//lib\internal\modules\cjs\loader.js
// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call
// `NativeModule.prototype.compileForPublicLoader()` and return the exports.
// 3. Otherwise, create a new module for the file and save it to the cache.
// Then have it load the file contents before returning its exports
// object.
// request 是申请模块的门路,这里对应着 './testa'
// parent 是父模块 test 的信息
// isMain 是否主文件(入口文件),这里是 false
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}`;
const filename = relativeResolveCache[relResolveCacheIdentifier];
if (filename !== undefined) {const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
delete relativeResolveCache[relResolveCacheIdentifier];
}
}
// 失去申请模块的绝对路径
const filename = Module._resolveFilename(request, parent, isMain);
// 查问缓存,如果已缓存,则更新对应的模块并且返回缓存的模块
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
// 如果加载的是原生模块(c++ 模块),则判断 canBeRequiredByUsers 而后返回对应的模块
const mod = loadNativeModule(filename, request);
if (mod && mod.canBeRequiredByUsers) return mod.exports;
// 否则,新建 Module 实例,构造函数自身曾经调用了 updateChildren,这里不须要再调用
// Don't call updateChildren(), Module constructor already does.
const module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
// 建设缓存
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 {
// 执行 load 办法
module.load(filename);
}
threw = false;
} finally {if (threw) {delete Module._cache[filename];
if (parent !== undefined) {delete relativeResolveCache[relResolveCacheIdentifier];
const children = parent && parent.children;
if (ArrayIsArray(children)) {const index = children.indexOf(module);
if (index !== -1) {children.splice(index, 1);
}
}
}
}
}
// 最初返回 module.exports
return module.exports;
};
Module._load
次要是做了以下三件事:
- 如果模块曾经在缓存中,则间接返回缓存的对象
- 如果模块是原生模块(c++ 模块),则返回对应的模块
- 否则,创立一个
Module
实例,而后保留到缓存中,执行实例办法load
,最初返回实例属性exports
接下来咱们看看 module.load
做了什么。
// lib\internal\modules\cjs\loader.js
// 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;
// 取得 node_modules 的门路
this.paths = Module._nodeModulePaths(path.dirname(filename));
// 这里的 extension 是 js
const extension = findLongestRegisteredExtension(filename);
// allow .mjs to be overridden
if (filename.endsWith('.mjs') && !Module._extensions['.mjs']) {throw new ERR_REQUIRE_ESM(filename);
}
// 这里做了什么?Module._extensions[extension](this, filename);
this.loaded = true;
// 上面是 cjs 兼容 esm 的操作,这次先不剖析
const ESMLoader = asyncESM.ESMLoader;
const url = `${pathToFileURL(filename)}`;
const module = ESMLoader.moduleMap.get(url);
// Create module entry at load time to snapshot exports correctly
const exports = this.exports;
// Called from cjs translator
if (module !== undefined && module.module !== undefined) {if (module.module.getStatus() >= kInstantiated)
module.module.setExport('default', exports);
} else {
// Preemptively cache
// We use a function to defer promise creation for async hooks.
ESMLoader.moduleMap.set(
url,
// Module job creation will start promises.
// We make it a function to lazily trigger those promises
// for async hooks compatibility.
() => new ModuleJob(ESMLoader, url, () =>
new ModuleWrap(url, undefined, ['default'], function() {this.setExport('default', exports);
})
, false /* isMain */, false /* inspectBrk */)
);
}
};
Module.prototype.load
做了以下这些事:
- 调用
Module._extensions[extension](this, filename)
办法 - 标记已加载模块
- cjs 兼容 esm
接下来看看 Module._extensions[extension](this, filename)
做了什么
// lib\internal\modules\cjs\loader.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);
}
}
// 以 utf8 格局读取文件
const content = fs.readFileSync(filename, 'utf8');
// 编译
module._compile(content, filename);
};
Module.prototype.load
做了以下这些事:
- 以 utf8 格局读取模块文件,失去字符串
- 编译
上面看看 module._compile(content, filename)
是如何编译的
// lib\internal\modules\cjs\loader.js
// 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);
// 见下文,失去一个组装好的函数
/*
function (exports, require, module, __filename, __dirname) {
// 模块代码
module.exports = 'abc'
}
*/
const compiledWrapper = wrapSafe(filename, content, this);
var inspectorWrapper = null;
if (getOptionValue('--inspect-brk') && process._eval == null) {if (!resolvedArgv) {
// We enter the repl if we're not given a filename argument.
if (process.argv[1]) {
try {resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
} catch {
// We only expect this codepath to be reached in the case of a
// preloaded module (it will fail earlier with the main entry)
assert(ArrayIsArray(getOptionValue('--require')));
}
} else {resolvedArgv = 'repl';}
}
// Set breakpoint on module start
if (resolvedArgv && !hasPausedEntry && filename === resolvedArgv) {
hasPausedEntry = true;
inspectorWrapper = internalBinding('inspector').callAndPauseOnStart;
}
}
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 {
/* 执行组装好的函数
call 办法的 this,指向 exports。所以在 cjs 模块里间接 console.log(this)后果是{},而非 global 对象
exports,指向 module 实例的 exports 属性,值为{}
require,就是加载模块的办法自身
module,module = this,this 是 module 实例对象,包含模块的一些信息
__filename,其实就是模块的绝对路径
__dirname,其实就是调用 path.dirname 获取该模块的文件夹门路
*/
result = compiledWrapper.call(thisValue, exports, require, module,
filename, dirname);
}
hasLoadedAnyUserCJSModule = true;
if (requireDepth === 0) statCache = null;
// 返回执行后果
return result;
};
// lib\internal\modules\cjs\loader.js
let wrap = function(script) {return Module.wrapper[0] + script + Module.wrapper[1];
};
const wrapper = ['(function (exports, require, module, __filename, __dirname) {',
'\n});',
];
function wrapSafe(filename, content, cjsModuleInstance) {
// 补丁办法
if (patched) {
/* 组装函数,成果如下:(function (exports, require, module, __filename, __dirname) {
// 模块代码
module.exports = 'abc'
});
*/
const wrapper = Module.wrap(content);
// 应用 node 虚拟机的沙箱办法,返回组装好的函数
return vm.runInThisContext(wrapper, {
filename,
lineOffset: 0,
displayErrors: true,
importModuleDynamically: async (specifier) => {
const loader = asyncESM.ESMLoader;
return loader.import(specifier, normalizeReferrerURL(filename));
},
});
}
// 上面是应用了 c ++ 的外部办法 compileFunction,成果同上,就不剖析了
let compiled;
try {
compiled = compileFunction(
content,
filename,
0,
0,
undefined,
false,
undefined,
[],
[
'exports',
'require',
'module',
'__filename',
'__dirname',
]
);
} catch (err) {if (process.mainModule === cjsModuleInstance)
enrichCJSError(err);
throw err;
}
const {callbackMap} = internalBinding('module_wrap');
callbackMap.set(compiled.cacheKey, {importModuleDynamically: async (specifier) => {
const loader = asyncESM.ESMLoader;
return loader.import(specifier, normalizeReferrerURL(filename));
}
});
return compiled.function;
}
module._compile
做了以下这些事:
-
联合模块读出来的文本内容,组装模块成为这样的字符串
(function (exports, require, module, __filename, __dirname) { // 模块代码 module.exports = 'abc' });
- 通过
vm.runInThisContext
虚拟机沙箱返回函数 - 执行函数,并且注入变量
3. 入口模块是如何加载的
其实在一开始断点的时候曾经揭示了。咱们能够看到 调用堆栈
,其实就是咱们下面剖析的过程。只不过这里是间接调用 Module._load
来加载模块,而子模块是调用工具办法封装好的 makeRequireFunction
办法来调用。
4. 总结
4.1 require
的执行次要过程
- 如果模块曾经在缓存中,则间接返回缓存的对象
- 如果模块是原生模块(c++ 模块),则返回对应的模块
- 否则,创立一个
Module
实例,而后保留到缓存中 - 以
utf8
格局读取模块内容 -
组装函数字符串
(function (exports, require, module, __filename, __dirname) { // 模块代码 module.exports = 'abc' });
- 通过
vm.runInThisContext
虚拟机沙箱返回函数 - 执行函数,并且注入变量
- cjs 兼容 esm
- 返回实例属性
module.exports
4.2 从源码中揭示了哪些景象
- 在 cjs 模块里间接
console.log(this)
后果是{}
,而非 global 对象。因为 cjs 模块实质是一个封装好的函数,而且执行的时候应用call
绑定了this
为module
实例的属性exports
,其值为{}
- 在 cjs 模块中,module.exports === exports,都是指向
module
实例的属性exports
- nodejs 文档中说
exports, require, module, __filename, __dirname
都不是全局对象,其实是注入的变量
正文完