node模块化加载源码

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()

更多的是内置模块、文件模块的判断以及加载缓存,但不是文件模块加载的核心处理方法

  1. 如果该模块已经被加载入缓存,直接导出该缓存对象
  2. 如果该模块是内置模块,调用NativeModule.prototype.compileForPublicLoader()然后导出
  3. 都不是以上情况,创建新的模块然后添加到缓存加载文件内容后再导出对象
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;图示

    image-20220529153328050

    只是创建了一个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++扩展文件

    image-20220529155506227

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模块化都会以这种形式进行包裹

    image-20220529162613098

    《深入浅出Node - 朴灵》19页 2.2 Node的模块实现

    1. JavaScript模块的编译
    (function (exports, require, module, __filename, __dirname) {
      var math = require('math');
      exports.area = function (radius) {
        return Math.PI * radius * radius;
      };
    }); 
    

图解CJS加载流程

iShot_2022-05-29_17.14.41

require()简单实现

  • 缓存优先 ✅
  • vm加载 ✅
  • 模块使用匿名函数封装,this使用exports、dirname、filename、require传参

只是简单实现,了解require源码

传送门:https://github.com/JYbill/node-awesome-part–knowledge

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值