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 是否主文件(入口文件),这里是falseModule._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 .jsModule._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.jslet 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
都不是全局对象,其实是注入的变量