1. 指标

摸索 Node.jsrequire 办法是如何实现的

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.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 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 次要是做了以下三件事:

  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 .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 做了以下这些事:

  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 = 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 做了以下这些事:

  1. 联合模块读出来的文本内容,组装模块成为这样的字符串

    (function (exports, require, module, __filename, __dirname) {  // 模块代码 module.exports = 'abc'});
  2. 通过 vm.runInThisContext 虚拟机沙箱返回函数
  3. 执行函数,并且注入变量

3. 入口模块是如何加载的

其实在一开始断点的时候曾经揭示了。咱们能够看到 调用堆栈 ,其实就是咱们下面剖析的过程。只不过这里是间接调用 Module._load 来加载模块,而子模块是调用工具办法封装好的 makeRequireFunction 办法来调用。

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 都不是全局对象,其实是注入的变量