Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【node源码】module源码阅读 #16

Open
EasonYou opened this issue May 19, 2020 · 0 comments
Open

【node源码】module源码阅读 #16

EasonYou opened this issue May 19, 2020 · 0 comments

Comments

@EasonYou
Copy link
Owner

module 模块

模块封装器

在Node.js模块系统中,每个文件都被视为一个独立的模块

在执行模块之前,Node.js会使用一个如下的封装器进行封装

(function(exports, require, module, __filename, __dirname) {
// 模块的代码实际上在这里
});

通过这样,实现了以下几点

  • 它保持了顶层的变量(用 var、 const 或 let 定义)作用在模块范围内,而不是全局对象。
  • 它有助于提供一些看似全局的但实际上是模块特定的变量,如moduleexports对象,以及__filename__dirname

模块作用域

  • __dirname获取当前模块的目录名
  • __filename当前模块的文件名的绝对路径
  • exportsmodule.exports的简短引用形式
  • module对当前模块的引用
  • require.resolve.paths,返回一个数组,其中包含解析 request 过程中被查询的路径(核心模块返回null)

主模块访问

运行一个文件时,require.main会被设为它的module
可以通过 require.main === module 来判断一个文件是否被直接运行

require.resolve

用来查询某个模块文件的带有完整绝对路径的文件名

// 输出 /home/user/test.js
require.resolve('./test');

缓存

模块在第一次加载后,会被缓存,每次调用同个模块,返回相同的对象

注意事项

模块是基于其解析的文件名进行缓存的。 由于调用模块的位置的不同,模块可能被解析成不同的文件名(比如从 node_modules 目录加载),这样就不能保证 require('foo') 总能返回完全相同的对象。

有些系统,区分大小写

require.cache

被引入的模块将被缓存在这个对象中,从此对象中删除键值对将会导致下一次 require 重新加载被删除的模块,不使用于原生插件。

核心模块

require() 总是会优先加载核心模块。 例如, require('http') 始终返回内置的 HTTP 模块,即使有同名文件。

核心模块在node源代码的lib/目录下

循环(重点)

参照实例代码的输出

当 main.js 加载 a.js 时, a.js 又加载 b.js。 此时, b.js 会尝试去加载 a.js。 为了防止无限的循环,会返回一个 a.js 的 exports 对象的未完成的副本(空对象)给 b.js 模块。 然后 b.js 完成加载,并将 exports 对象提供给 a.js 模块。

文件模块

没有确切的文件名,会尝试带上.js.json.node拓展名再加载
.node文件会被解析为通过process.dlopen()加载的编译后的插件模块
当没有以 '/'、 './' 或 '../' 开头来表示文件时,这个模块必须是一个核心模块或加载自 node_modules 目录。

目录作为模块

package.jsonmain属性,指定一个模块,如果没有,则试图加载目录下的index.js或者index.node文件

从 node_modules 目录加载

会从当前目录的node_modules查找,然后以此往上找到文件系统的根目录

源码阅读

require

require通过makeRequireFunction方法来创建

// makeRequireFunction在global上定义,require === this.require
ObjectDefineProperty(context, 'require', {
    configurable: true,
    writable: true,
    value: makeRequireFunction(module)
  });


// 从上面可以看出,mod就是module
function makeRequireFunction(mod, redirects) {
  const Module = mod.constructor;

  let require;
  if (redirects) {
    // redirects的情况忽略
  } else {
    // require的方法定义
    require = function require(path) {
      // 调用的是module.require
      return mod.require(path);
    };
  }
  // resolve方法
  function resolve(request, options) {
    validateString(request, 'request');
    return Module._resolveFilename(request, mod, false, options);
  }
  require.resolve = resolve;
  // path方法
  function paths(request) {
    validateString(request, 'request');
    return Module._resolveLookupPaths(request, mod);
  }

  resolve.paths = paths;
  // 在新版本中,不可自定义的extensions
  require.extensions = Module._extensions;
  // 模块缓存挂载
  require.cache = Module._cache;
  return require;
}

从上面可以得知,最后require的调用,是Module.prototype.require,最终调用的是Module._load

  • 如果模块已经在缓存中存在,返回其模块暴露出的对象
  • 如果模块是原生模块,则会调用NativeModule.prototype.compileForPublicLoader()然后返回暴露的对象
  • 其他情况下,生成新的模块再保存到缓存中,然后让它在返回其导出对象之前加载文件内容
// 直接调用的是Module._load
Module.prototype.require = function(id) {
  // ..
  try {
    return Module._load(id, this, /* isMain */ false);
  } finally { /** .. */ }
};

Module._load = function(request, parent, isMain) {
  let relResolveCacheIdentifier;
  // ...
  // 获取文件名
  const filename = Module._resolveFilename(request, parent, isMain);
  // 获取缓存,如果有缓存的话,直接返回缓存
  const cachedModule = Module._cache[filename];
  if (cachedModule !== undefined) {
    // 把模块放到module.children中
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }
  // 优先读取原生模块,如果有原生模块且可以给用户调用,则暴露出去
  const mod = loadNativeModule(filename, request, experimentalModules);
  if (mod && mod.canBeRequiredByUsers) return mod.exports;
  // 如果不是原生模块,通过new Module建立模块实例
  const module = new Module(filename, parent);
  // 如果是主模块,绑定到process上
  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }
  // 把模块放到缓存中
  Module._cache[filename] = module;
  // 存放文件名
  if (parent !== undefined) {
    relativeResolveCache[relResolveCacheIdentifier] = filename;
  }

  let threw = true;
  try {
    if (enableSourceMaps) {
      try {
        // 最终调用module.load
        module.load(filename);
      } catch (err) { /** ... */ }
    } else {
      module.load(filename);
    }
    threw = false;
  } finally { /** ... */ }

  return module.exports;
};

在这里可以看出

  • 原生模块是优先级最高的
  • 有缓存读缓存,没缓存生成Module实例
  • 所有的模块都是Module的实例,包括缓存上的模块

接下来看下load方法

Module.prototype.load = function(filename) {
  // 这里来获取文件地址以及它的扩展名
  this.filename = filename;
  this.paths = Module._nodeModulePaths(path.dirname(filename));
  const extension = findLongestRegisteredExtension(filename);
  // ...
  // 这里来读取文件,然后设置loaded为true,表示已经加载完毕
  Module._extensions[extension](this, filename);
  this.loaded = true;
  // ...
};

那么,从这里看出,load方法其实就是调用Module._extensions上对应拓展名的方法

接下来,看下对应的方法是怎么去load的

js文件的读取

js文件通过读取其文件,再进行compile。其中,_compile就是做了

Module._extensions['.js'] = function(module, filename) {
  if (filename.endsWith('.js')) {
    // ..
  }
  const content = fs.readFileSync(filename, 'utf8');
  module._compile(content, filename);
};

Module.prototype._compile = function(content, filename) {
  let moduleURL;
  let redirects;
  // ..
  // 这里生成一个编译方法
  const compiledWrapper = wrapSafe(filename, content, this);

  var inspectorWrapper = null;
  // ..
  if (inspectorWrapper) {
    // ..
  } else {
    // 调用上面生成的编译方法
    result = compiledWrapper.call(thisValue, exports, require, module,
                                  filename, dirname);
  }
  // ..
  return result;
};
// 返回了编译后的方法
function wrapSafe(filename, content, cjsModuleInstance) {
  if (patched) {
    // 这里做外层的包裹
    const wrapper = Module.wrap(content);
    // 通过vm,来生成把代码在当前上下文去执行
    // wrapper,是一串代码字符串
    return vm.runInThisContext(wrapper, { /** ... */ });
  }
  // ...
  let compiled;
  try {
    // 调用原生模块做方法编译
    compiled = compileFunction();
  } catch (err) { /** ... */ }
  // ..
  return compiled.function;
}
// 这里,对读取的代码字符串,做了外层包裹,并把全局参数传入
let wrap = function(script) {
  return Module.wrapper[0] + script + Module.wrapper[1];
};
const wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

json文件的读取

json文件较为简单,直接读取文件字符串,然后解析json

Module._extensions['.json'] = function(module, filename) {
  const content = fs.readFileSync(filename, 'utf8'); // 读取文件
  // 
  try {
    // 解析json
    module.exports = JSONParse(stripBOM(content));
  } catch (err) { /** ... */ }
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant