node v14源码分析之文件加载

1,307 阅读5分钟

本篇文章不涉及c++代码部分

收获?

  1. 执行node发生了什么
  2. require执行流程
  3. 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]);

入口文件做了什么?

  1. 找到原生模块internal/modules/cjs/loader
  2. 使用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

主逻辑如下:

  1. 获得绝对路径,主要是拿到文件后缀
  2. 绝对路径作为key在Module._cache中查找是否加载过
  3. 如果缓存中有值,直接返回该模块的module.exports,反之走下一步
  4. 判断是否是原生模块,若是则使用loadNativeModule加载并返回结果,反之走下一步
  5. 初始化一个普通的module:const module = new Module(filename, parent);,并将其缓存到Module._cache(注意此时缓存中只是初始化的module)
  6. 使用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的加载

主要步骤:

  1. 加载文件内容
  2. 开始编译
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文件

  1. 生成沙盒环境const compiledWrapper = wrapSafe(filename, content, this);
  2. 使用沙盒环境调用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));
};