require源码分析

1. 目标
【require源码分析】探索 Node.jsrequire 方法是如何实现的
2. 调试方法
2.1 点击添加配置 require源码分析
文章图片

2.2 配置相关信息 这里需要注意的是,把 skipFiles 需要把 /** 注释掉,这样才能够 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": [ // "/**" ] } ] }

3. require执行的过程是怎样的
require源码分析
文章图片

在第八行打断点,之后就可以点击debug按钮了,我们可以看到调用堆栈中,目前停止执行的函数。单步调试。
require源码分析
文章图片

可以看到是调用了一个工具函数,最终调用了 mod.require 方法。继续单步调用。
require源码分析
文章图片

上面的 mod.require 调用的是 loader.jsModule.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 loadthe 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 主要是做了以下三件事:
  1. 如果模块已经在缓存中,则直接返回缓存的对象
  2. 如果模块是原生模块(c++模块),则返回对应的模块
  3. 否则,创建一个 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 做了以下这些事:
  1. 调用 Module._extensions[extension](this, filename) 方法
  2. 标记已加载模块
  3. 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 做了以下这些事:
  1. 以utf8格式读取模块文件,得到字符串
  2. 编译
下面看看 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 = https://www.it610.com/article/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 做了以下这些事:
  1. 结合模块读出来的文本内容,组装模块成为这样的字符串
    (function (exports, require, module, __filename, __dirname) { // 模块代码 module.exports = 'abc' });

  2. 通过 vm.runInThisContext 虚拟机沙箱返回函数
  3. 执行函数,并且注入变量
3. 入口模块是如何加载的
其实在一开始断点的时候已经揭示了。我们可以看到 调用堆栈 ,其实就是我们上面分析的过程。只不过这里是直接调用 Module._load 来加载模块,而子模块是调用工具方法封装好的 makeRequireFunction 方法来调用。
require源码分析
文章图片

4. 总结
4.1 require的执行主要过程
  1. 如果模块已经在缓存中,则直接返回缓存的对象
  2. 如果模块是原生模块(c++模块),则返回对应的模块
  3. 否则,创建一个 Module 实例,然后保存到缓存中
  4. utf8格式读取模块内容
  5. 组装函数字符串
    (function (exports, require, module, __filename, __dirname) { // 模块代码 module.exports = 'abc' });

  6. 通过 vm.runInThisContext 虚拟机沙箱返回函数
  7. 执行函数,并且注入变量
  8. cjs兼容esm
  9. 返回实例属性 module.exports
4.2 从源码中揭示了哪些现象
  1. 在cjs模块里直接 console.log(this) 结果是 {},而非global对象。因为cjs模块本质是一个封装好的函数,而且执行的时候使用 call 绑定了 thismodule 实例的属性 exports,其值为 {}
  2. 在cjs模块中,module.exports === exports,都是指向 module 实例的属性 exports
  3. nodejs文档中说 exports, require, module, __filename, __dirname 都不是全局对象,其实是注入的变量

    推荐阅读