node模块化加载源码
-
代码
// 13-module-source.js // 对其debug const obj = require('./13-module-export'); // 13-module-export.js module.exports = { name: 'xiaoqinvar' }
helpers.js#makeRequireFunction()
加载模块的方法
require = function require(path) {
return mod.require(path); // debug到此,不用管,继续往下
};
helpers.js#Module.prototype.require()
require()方法,但不是真正处理加载逻辑的
// id就是我们传过来的表示符,即“./13-module-export”文件路径
Module.prototype.require = function(id) {
validateString(id, 'id');
if (id === '') {
throw new ERR_INVALID_ARG_VALUE('id', id,
'must be a non-empty string');
}
requireDepth++;
try {
// 加载的方法,继续进去
return Module._load(id, this, /* isMain */ false);
} finally {
requireDepth--;
}
};
loader.js#Module._load()
更多的是内置模块、文件模块的判断以及加载缓存,但不是文件模块加载的核心处理方法。
- 如果该模块已经被加载入缓存,直接导出该缓存对象
- 如果该模块是内置模块,调用
NativeModule.prototype.compileForPublicLoader()
然后导出- 都不是以上情况,
创建新的模块
然后添加到缓存
,加载文件内容
后再导出对象
Module._load = function(request, parent, isMain) {
let relResolveCacheIdentifier;
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
// 缓存标识符
relResolveCacheIdentifier = `${parent.path}\x00${request}`;
// 在缓存中查找该相对路径的标识符是否存在?存在直接导出
// 第一次肯定直接跳过
const filename = relativeResolveCache[relResolveCacheIdentifier];
if (filename !== undefined) {
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
if (!cachedModule.loaded)
return getExportsForCircularRequire(cachedModule);
return cachedModule.exports; // 1. 存在缓存模块情况
}
delete relativeResolveCache[relResolveCacheIdentifier];
}
}
// 处理标识符,是内置模块?还是用户定义/第三方的文件模块?
const filename = Module._resolveFilename(request, parent, isMain);
// 2. 这里校验的是否是核心模块,是的话直接导出,目前高版本node标识符都是类似'node:fs/promise'
if (StringPrototypeStartsWith(filename, 'node:')) {
// Slice 'node:' prefix
const id = StringPrototypeSlice(filename, 5);
const module = loadNativeModule(id, request);
if (!module?.canBeRequiredByUsers) {
throw new ERR_UNKNOWN_BUILTIN_MODULE(filename);
}
// 内置模块导出
return module.exports;
}
// 查找是否有缓存绝对路径的文件标识符
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
if (!cachedModule.loaded) {
const parseCachedModule = cjsParseCache.get(cachedModule);
if (!parseCachedModule || parseCachedModule.loaded)
return getExportsForCircularRequire(cachedModule);
parseCachedModule.loaded = true;
} else {
return cachedModule.exports;
}
}
// 2. 尝试去加载内置模块'http' 'fs'这种,我们是文件模块,肯定没有
const mod = loadNativeModule(filename, request);
if (mod && mod.canBeRequiredByUsers) return mod.exports;
// 以上排除了内置模块和缓存,说明是一个新文件模块,二话不锁实例化一个
const module = cachedModule || new Module(filename, parent);
// 跳过,不是main
if (isMain) {
process.mainModule = module;
module.id = '.';
}
// 将绝对路径作为缓存key,并缓存,但此时缓存的是未执行文件模块的
// 你的debug可以查看到该缓存对象的exports = {}
Module._cache[filename] = module;
// 相对路径缓存进行缓存了一下
if (parent !== undefined) {
relativeResolveCache[relResolveCacheIdentifier] = filename;
}
let threw = true;
try {
// 3. 真正的文件模块加载处理器来咯~
module.load(filename);
// 状态机策略,如果load()抛出异常,那么threw为true,就会执行异常处理
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 = ArrayPrototypeIndexOf(children, module);
if (index !== -1) {
ArrayPrototypeSplice(children, index, 1);
}
}
}
} else if (module.exports &&
!isProxy(module.exports) &&
ObjectGetPrototypeOf(module.exports) ===
CircularRequirePrototypeWarningProxy) {
ObjectSetPrototypeOf(module.exports, ObjectPrototype);
}
}
// 在module实例化后 -> module加载完成 -> module缓存后
// 导出13-module-export.js的exports
return module.exports;
};
-
Module._cache[filename] = module;图示
只是创建了一个module实例独享,但为读取该文件模块内容和导出
loader.js#Module.prototype.load()
其实这里也不是真正解析js文件模块逻辑,只是让通过文件名,使用对应的扩展名处理进行加载,EJS缓存
Module.prototype.load = function(filename) {
debug('load %j for module %j', filename, this.id);
assert(!this.loaded);
// 看这个filename熟悉不?
// 绝对路径文件名
this.filename = filename;
//⚠️ 看这个paths熟悉不?不熟悉看看下面打印的module,这里的this指向刚才创建的module实例!
// 迭代查找的路径,查node_modules,递归查找
this.paths = Module._nodeModulePaths(path.dirname(filename));
// 因为一个文件可以能有多个"a.b.c.js"嘛,这里查的是最后一个,查不到默认以'.js'返回
// 我这里返回的肯定是`.js`
const extension = findLongestRegisteredExtension(filename);
// 扩展名`.mjs`的抛出错误引入ESM模块错误,ESM可以引入CJS,但CJS不允许导入ESM
if (StringPrototypeEndsWith(filename, '.mjs') && !Module._extensions['.mjs'])
throw new ERR_REQUIRE_ESM(filename);
// 这里可以看看下面的图,这个_extensions[]存的是三个方法,分别对应解析
// 伪代码:['js'(), '.json'(), '.node'()],去用对应的处理方法执行该文件模块
// this指代刚才module实例化
// Module._extensions是个静态方法!
Module._extensions[extension](this, filename);
// 该模块加载完成
this.loaded = true;
const ESMLoader = asyncESM.ESMLoader;
// Create module entry at load time to snapshot exports correctly
const exports = this.exports; // 13-module-export.js文件导出的内容
// Preemptively cache
// 因为ESM可以导入CJS所以这里ESM缓存了一份CJS的缓存,下次ESM导入就可以直接拿缓存了
if ((module?.module === undefined ||
module.module.getStatus() < kEvaluated) &&
!ESMLoader.cjsCache.has(this))
ESMLoader.cjsCache.set(this, exports);
};
-
让你熟悉熟悉
console.log(module); Module { id: '.', path: '/Users/xiaoqinvar/Desktop/practice/node高级/packages/node高级/src', exports: {}, parent: null, filename: '/Users/xiaoqinvar/Desktop/practice/node高级/packages/node高级/src/temp-test.js', loaded: false, children: [], paths: [ '/Users/xiaoqinvar/Desktop/practice/node高级/packages/node高级/src/node_modules', '/Users/xiaoqinvar/Desktop/practice/node高级/packages/node高级/node_modules', '/Users/xiaoqinvar/Desktop/practice/node高级/packages/node_modules', '/Users/xiaoqinvar/Desktop/practice/node高级/node_modules', '/Users/xiaoqinvar/Desktop/practice/node_modules', '/Users/xiaoqinvar/Desktop/node_modules', '/Users/xiaoqinvar/node_modules', '/Users/node_modules', '/node_modules' ] }
-
Module._extensions:保存三个处理器,分别处理不同扩展名文件,js文件、json文件、node c++扩展文件
loader.js#Module._extensions[‘.js’]()
🔥这里真真真的是js文件模块处理器真实逻辑
Module._extensions['.js'] = function(module, filename) {
if (StringPrototypeEndsWith(filename, '.js')) {
// node 包机制,读取包信息
const pkg = readPackageScope(filename);
// require()方法不应该用于ESM中
if (pkg && pkg.data && pkg.data.type === 'module') {
const { parent } = module;
const parentPath = parent && parent.filename;
const packageJsonPath = path.resolve(pkg.path, 'package.json');
throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath);
}
}
// 如果解析过模块源文件将会被缓存起来,所以试图从cjs解析缓存中获取该模块
// 咱们都没加载过那个模块文件肯定是没有的
const cached = cjsParseCache.get(module);
let content;
if (cached && cached.source) {
content = cached.source;
cached.source = undefined;
} else {
// 🔥 为什么别人说require()是同步的就是这里!
// utf8读取源文件字符串内容,就是我们编写的代码
content = fs.readFileSync(filename, 'utf8');
// content: 'module.exports = {\n name: 'xiaoqinvar'\n}'
}
// 编译执行模块
module._compile(content, filename);
};
接下来面临的问题是,既然读取到了文件模块内容,那怎么将它执行呢?执行之后导出的问题!
loader.js#Module.prototype._compile()
运行读取出来的字符串(使用沙箱机制)
Module.prototype._compile = function(content, filename) {
let moduleURL;
let redirects;
if (policy?.manifest) {
moduleURL = pathToFileURL(filename);
redirects = policy.manifest.getDependencyMapper(moduleURL);
policy.manifest.assertIntegrity(moduleURL, content);
}
maybeCacheSourceMap(filename, content, this);
// 使用vm沙箱机制,去执行并返回需要导出的内容
const compiledWrapper = wrapSafe(filename, content, this);
let 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 SafeMap();
if (inspectorWrapper) {
result = inspectorWrapper(compiledWrapper, thisValue, exports,
require, module, filename, dirname);
} else {
// 这里会执行wrapSafe()包装的函数,this = thisValue = exports = this.exports = {}
// 这也就是为什么node中打印this指向一个空对象
result = ReflectApply(compiledWrapper, thisValue,
[exports, require, module, filename, dirname]);
}
hasLoadedAnyUserCJSModule = true;
if (requireDepth === 0) statCache = null;
return result; // 返回导出对象,即 13-module-export.js文件的导出内容
};
-
不信this === exports? 不信他是导出的对象?
// test.js console.log(this); console.log(this === exports); this.name = 'xiaoqinvar'; // res.js const obj = require('./13-module-export'); console.log(obj); // 结果 👌 {} // this true // this === exports { name: 'xiaoqinvar' } // obj
loader.js#wrapSafe()
对读取出来的字符串使用compileFunction()进行包装
function wrapSafe(filename, content, cjsModuleInstance) {
// 这里会跳过
if (patched) {
const wrapper = Module.wrap(content);
return vm.runInThisContext(wrapper, {
filename,
lineOffset: 0,
displayErrors: true,
importModuleDynamically: async (specifier) => {
const loader = asyncESM.ESMLoader;
return loader.import(specifier, normalizeReferrerURL(filename));
},
});
}
// 包装结果
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;
}
-
compileFunction返回结果图
这里证实了《深入浅出Node - 朴灵》TJ大大的结论,所有cjs模块化都会以这种形式进行包裹《深入浅出Node - 朴灵》19页 2.2 Node的模块实现
- JavaScript模块的编译
(function (exports, require, module, __filename, __dirname) { var math = require('math'); exports.area = function (radius) { return Math.PI * radius * radius; }; });
图解CJS加载流程
require()简单实现
- 缓存优先 ✅
- vm加载 ✅
- 模块使用匿名函数封装,this使用exports、dirname、filename、require传参
只是简单实现,了解require源码