本篇文章不涉及c++代码部分
收获?
执行node发生了什么require执行流程node如何解决循环依赖
node命令是如何执行的?
这里先展示一段简单的脚本并运行它
a.js
console.log('hello js');
运行:node a
入口文件
找到入口文件:node/lib/internal/modules/cjs/loader.js
require('internal/modules/cjs/loader').Module.runMain(process.argv[1]);
入口文件做了什么?
- 找到原生模块
internal/modules/cjs/loader - 使用
Module.runMain加载入口文件(process.argv[1]的值是a.js的绝对路径去掉后缀名)
进入Module.runMain
function executeUserEntryPoint(main = process.argv[1]) {
// 查找绝对路径,判断是否是es模块,这里不做探究
const resolvedMain = resolveMainPath(main);
const useESMLoader = shouldUseESMLoader(resolvedMain);
if (useESMLoader) {
runMainESM(resolvedMain || main);
} else {
// 加载cjs模块
Module._load(main, null, true);
}
}
cjs加载入口_load
主逻辑如下:
- 获得绝对路径,主要是拿到文件后缀
- 绝对路径作为key在
Module._cache中查找是否加载过 - 如果缓存中有值,直接返回该模块的
module.exports,反之走下一步 - 判断是否是原生模块,若是则使用
loadNativeModule加载并返回结果,反之走下一步 - 初始化一个普通的module:
const module = new Module(filename, parent);,并将其缓存到Module._cache(注意此时缓存中只是初始化的module) - 使用
module.load(filename);开始加载并给module.exports赋值
Module._load = function(request, parent, isMain) {
let relResolveCacheIdentifier;
//这个filename是怎么拿到的,先不关注,只要知道经过这个函数,可以加上文件扩展名
const filename = Module._resolveFilename(request, parent, isMain);
//查看是否有缓存(第一次加载没有缓存,可以忽略)
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
// 查看当前模块是否已经加载完成,若没有则判断为循环依赖
if (!cachedModule.loaded)
// 可以理解为就是return module.exports;
return getExportsForCircularRequire(cachedModule);
// 直接走缓存
return cachedModule.exports;
}
//查看是否是原生模块
const mod = loadNativeModule(filename, request);
if (mod && mod.canBeRequiredByUsers) return mod.exports;
// Don't call updateChildren(), Module constructor already does.
//如果是普通模块就创建一个模块,这里面可以看到里面的实现,exports被赋上了一个默认值{}
const module = new Module(filename, parent);
// 主模块被传入默认值true
if (isMain) {
process.mainModule = module;
module.id = '.';
}
//给缓存赋值
Module._cache[filename] = module;
//忽略
if (parent !== undefined) {
relativeResolveCache[relResolveCacheIdentifier] = filename;
}
let threw = true;
//这里不关注和SourceMap相关的逻辑
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 {
//这里是主要逻辑,如果该模块加载失败,会在下面删除掉缓存上的值delete Module._cache[filename];
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);
}
}
}
} else if (module.exports &&
!isProxy(module.exports) &&
ObjectGetPrototypeOf(module.exports) ===
CircularRequirePrototypeWarningProxy) {
ObjectSetPrototypeOf(module.exports, PublicObjectPrototype);
}
}
return module.exports;
};
真实加载入口 module.load
这里其实就是调用Module._extensions[extension](this, filename);来加载
Module.prototype.load = function(filename) {
this.filename = filename;
this.paths = Module._nodeModulePaths(path.dirname(filename));
const extension = findLongestRegisteredExtension(filename);
// allow .mjs to be overridden
if (filename.endsWith('.mjs') && !Module._extensions['.mjs']) {
throw new ERR_REQUIRE_ESM(filename);
}
/*
* 这里开始加载模块,加载完成会紧接着 将loaded设置位true
* 这里后缀为js我们来看看其如何加载
* */
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;
// Preemptively cache
if ((module?.module === undefined ||
module.module.getStatus() < kEvaluated) &&
!ESMLoader.cjsCache.has(this))
ESMLoader.cjsCache.set(this, exports);
};
接下来我们主要来看后缀为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 { parent } = module;
const parentPath = parent && parent.filename;
const packageJsonPath = path.resolve(pkg.path, 'package.json');
throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath);
}
}
// 直接读取文件内容,这里我们可以知道为什么js文件过大会导致内存溢出
const content = fs.readFileSync(filename, 'utf8');
// 开始编译源文件
module._compile(content, filename);
};
编译js文件
- 生成沙盒环境
const compiledWrapper = wrapSafe(filename, content, this); - 使用沙盒环境调用js文件,此时exports上赋值完成
Module.prototype._compile = function(content, filename) {
const compiledWrapper = wrapSafe(filename, content, this);
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 {
// 这里开始执行生成的函数,进入入口文件,至此我们知道加载js做了什么
result = compiledWrapper.call(thisValue, exports, require, module,
filename, dirname);
}
hasLoadedAnyUserCJSModule = true;
if (requireDepth === 0) statCache = null;
return result;
};
到此node加载完成第一个文件
require运行原理
从以下代码中我们可以看到,其核心就是调用了Module._load(id, this, /* isMain */ false);这行代码,流程其实和之前加载流程相同
//require入口
require = function require(path) {
return mod.require(path);
};
Module.prototype.require = function(id) {
// id合法检查
validateString(id, 'id');
if (id === '') {
throw new ERR_INVALID_ARG_VALUE('id', id,
'must be a non-empty string');
}
// 依赖深度
requireDepth++;
try {
// 加载模块对比主模块 Module._load(main, null, true); 以下代码和主模块加载几乎一样
return Module._load(id, this, /* isMain */ false);
} finally {
requireDepth--;
}
};
// 循环依赖,缓存如何处理?
循环依赖如何解决?
动手画画流程图,以上流程理解的话基本就可以理解了,提示:当存在循环依赖时,由于模块被加载并执行到一半,node会从缓存中拿到已经加载的模块,继续执行。
require加载顺序?
上面流程中,我们在最终调用了Module._extensions[extension](this, filename);,来加载文件,所以这里的后缀名的获取流程就值得我们关注了。
还记得在哪里获取后缀的么?没错就是在cjs加载函数中,第一步就要拿到filename
const filename = Module._resolveFilename(request, parent, isMain);
Module._resolveFilename = function(request, parent, isMain, options) {
...
const filename = Module._findPath(request, paths, isMain, false);
if (filename) return filename;
...
}
Module._findPath = function(request, paths, isMain) {
...
exts = ObjectKeys(Module._extensions);
filename = tryExtensions(basePath, exts, isMain);
if (filename) {
Module._pathCache[cacheKey] = filename;
return filename;
}
...
}
function tryExtensions(p, exts, isMain) {
for (let i = 0; i < exts.length; i++) {
const filename = tryFile(p + exts[i], isMain);
if (filename) {
return filename;
}
}
return false;
}
通过上面的代码我们知道就是更具exts = ObjectKeys(Module._extensions);中的顺序来获取到后缀的,所以我们需要查看Module._extensions中的key怎么获得:
Module._extensions
此处我们可以清楚的看到,在这里定义了js、json、node这些后缀名文件,当然这仅仅只是相同目录下
export const ObjectCreate: typeof Object.create
Module._extensions = ObjectCreate(null);
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
if (StringPrototypeEndsWith(filename, '.js')) {
const pkg = readPackageScope(filename);
// Function require shouldn't be used in ES modules.
if (pkg?.data?.type === 'module') {
const parent = moduleParentCache.get(module);
const parentPath = 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);
let content;
if (cached?.source) {
content = cached.source;
cached.source = undefined;
} else {
content = fs.readFileSync(filename, 'utf8');
}
module._compile(content, filename);
};
// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
const content = fs.readFileSync(filename, 'utf8');
if (policy?.manifest) {
const moduleURL = pathToFileURL(filename);
policy.manifest.assertIntegrity(moduleURL, content);
}
try {
module.exports = JSONParse(stripBOM(content));
} catch (err) {
err.message = filename + ': ' + err.message;
throw err;
}
};
// Native extension for .node
Module._extensions['.node'] = function(module, filename) {
if (policy?.manifest) {
const content = fs.readFileSync(filename);
const moduleURL = pathToFileURL(filename);
policy.manifest.assertIntegrity(moduleURL, content);
}
// Be aware this doesn't use `content`
return process.dlopen(module, path.toNamespacedPath(filename));
};