Node.js Require源码粗读

3,271 阅读3分钟

作者:肖磊

个人主页:github

最近一直在用node.js写一些相关的工具,对于node.js的模块如何去加载,以及所遵循的模块加载规范的具体细节又是如何并不是了解。这篇文件也是看过node.js源码及部分文章总结而来:

es2015标准以前,js并没有成熟的模块系统的规范。Node.js为了弥补这样一个缺陷,采用了CommonJS规范中所定义的模块规范,它包括:

1.require

require是一个函数,它接收一个模块的标识符,用以引用其他模块暴露出来的API

2.module context

module context规定了一个模块当中,存在一个require变量,它遵从上面对于这个require函数的定义,一个exports对象,模块如果需要向外暴露API,即在一个exports的对象上添加属性。以及一个module object

3.module Identifiers

module Identifiers定义了require函数所接受的参数规则,比如说必须是小驼峰命名的字符串,可以没有文件后缀名,.或者..表明文件路径是相对路径等等。

具体关于commonJS中定义的module规范,可以参见wiki文档

在我们的node.js程序当中,我们使用require这个看起来是全局(后面会解释为什么看起来是全局的)的方法去加载其他模块。

const util = require('./util')

首先我们来看下关于这个方法,node.js内部是如何定义的:

Module.prototype.require = function () {
  assert(path, 'missing path');
  assert(typeof path === 'string', 'path must be a string');
  // 实际上是调用Module._load方法
  return Module._load(path, this, /* isMain */ false);
}

Module._load = function (request, parent, isMain) {
  .....

  // 获取文件名
  var filename = Module._resolveFilename(request, parent, isMain);

  // _cache缓存的模块
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

  // 如果是nativeModule模块
  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }

  // Don't call updateChildren(), Module constructor already does.
  // 初始化一个新的module
  var module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  // 加载模块前,就将这个模块缓存起来。注意node.js的模块加载系统是如何避免循环依赖的
  Module._cache[filename] = module;

  // 加载module
  tryModuleLoad(module, filename);

  // 将module.exports导出的内容返回
  return module.exports;
}

Module._load方法是一个内部的方法,主要是:

  1. 根据你传入的代表模块路径的字符串来查找相应的模块路径;
  2. 根据找到的模块路径来做缓存;
  3. 进而去加载对应的模块。

接下来我们来看下node.js是如何根据传入的模块路径字符串来查找对应的模块的:

Module._resolveFilename = function (request, parent, isMain, options) {
  if (NativeModule.nonInternalExists(request)) {
    return request;
  }

  var paths;

  if (typeof options === 'object' && options !== null &&
      Array.isArray(options.paths)) {
    ...
  } else {
    // 获取模块的大致路径 [parentDir]  | [id, [parentDir]]
    paths = Module._resolveLookupPaths(request, parent, true);
  }

  // look up the filename first, since that's the cache key.
  // node index.js
  // request = index.js
  // paths = ['/root/foo/bar/index.js', '/root/foo/bar']
  var filename = Module._findPath(request, paths, isMain);
  if (!filename) {
    var err = new Error(`Cannot find module '${request}'`);
    err.code = 'MODULE_NOT_FOUND';
    throw err;
  }
  return filename;
}

在这个方法内部,需要调用一个内部的方法:Module._resolveLookupPaths,这个方法会依据父模块的路径获取所有这个模块可能的路径:

Module._resolveLookupPaths = function (request, parent, newReturn) {
  ...
}

这个方法内部有以下几种情况的处理:

  1. 是启动模块,即通过node xxx启动的模块

这个时候node.js会直接获取到你这个程序执行路径,并在这个方法当中返回

  1. require(xxx)require一个存在于node_modules中的模块

这个时候会对执行路径上所有可能存在node_modules的路径进行遍历一遍

  1. require(./)require一个相对路径或者绝对路径的模块

直接返回父路径

当拿到需要找寻的路径后,调用Module._findPath方法去查找对应的文件路径。

Module._findPath = function (request, paths, isMain) {
  if (path.isAbsolute(request)) {
    paths = [''];
  } else if (!paths || paths.length === 0) {
    return false;
  }

  // \x00 -> null,相当于空字符串
  var cacheKey = request + '\x00' +
                (paths.length === 1 ? paths[0] : paths.join('\x00'));
  // 路径的缓存
  var entry = Module._pathCache[cacheKey];
  if (entry)
    return entry;

  var exts;
  // 尾部是否带有/
  var trailingSlash = request.length > 0 &&
                      request.charCodeAt(request.length - 1) === 47/*/*/;

  // For each path
  for (var i = 0; i < paths.length; i++) {
    // Don't search further if path doesn't exist
    const curPath = paths[i];   // 当前路径
    if (curPath && stat(curPath) < 1) continue;
    var basePath = path.resolve(curPath, request);
    var filename;

    // 调用internalModuleStat方法来判断文件类型
    var rc = stat(basePath);
    // 如果路径不以/结尾,那么可能是文件,也可能是文件夹
    if (!trailingSlash) {
      if (rc === 0) {  // File.  文件
        if (preserveSymlinks && !isMain) {
          filename = path.resolve(basePath);
        } else {
          filename = toRealPath(basePath);
        }
      } else if (rc === 1) {  // Directory. 当提供的路径是文件夹的情况下会去这个路径下找package.json中的main字段对应的模块的入口文件
        if (exts === undefined)
          // '.js' '.json' '.node' '.ms'
          exts = Object.keys(Module._extensions);
        // 获取pkg内部的main字段对应的值
        filename = tryPackage(basePath, exts, isMain);
      }

      if (!filename) {
        // try it with each of the extensions
        if (exts === undefined)
          exts = Object.keys(Module._extensions);
        filename = tryExtensions(basePath, exts, isMain); // ${basePath}.(js|json|node)等文件后缀,看是否文件存在
      }
    }

    // 如果路径以/结尾,那么就是文件夹
    if (!filename && rc === 1) {  // Directory.
      if (exts === undefined)
        exts = Object.keys(Module._extensions);
      filename = tryPackage(basePath, exts, isMain) ||
        // try it with each of the extensions at "index"
        tryExtensions(path.resolve(basePath, 'index'), exts, isMain);
    }

    if (filename) {
      // Warn once if '.' resolved outside the module dir
      if (request === '.' && i > 0) {
        if (!warned) {
          warned = true;
          process.emitWarning(
            'warning: require(\'.\') resolved outside the package ' +
            'directory. This functionality is deprecated and will be removed ' +
            'soon.',
            'DeprecationWarning', 'DEP0019');
        }
      }

      // 缓存路径
      Module._pathCache[cacheKey] = filename;
      return filename;
    }
  }
  return false;
}

function tryPackage(requestPath, exts, isMain) {
  var pkg = readPackage(requestPath); // 获取package.json当中的main字段

  if (!pkg) return false;

  var filename = path.resolve(requestPath, pkg);  // 解析路径
  return tryFile(filename, isMain) ||             // 直接判断这个文件是否存在
         tryExtensions(filename, exts, isMain) || // 判断这个分别以js,json,node等后缀结尾的文件是否存在
         tryExtensions(path.resolve(filename, 'index'), exts, isMain);  // 判断这个分别以 ${filename}/index.(js|json|node)等后缀结尾的文件是否存在
}

梳理下上面查询模块时的一个策略:

  1. require模块的时候,传入的字符串最后一个字符不是/时:
  • 如果是个文件,那么直接返回这个文件的路径

  • 如果是个文件夹,那么会找个这个文件夹下是否有package.json文件,以及这个文件当中的main字段对应的路径(对应源码当中的方法为tryPackage):

    • 如果main字段对应的路径是一个文件且存在,那么就返回这个路径
    • main字段对应的路径对应没有带后缀,那么尝试使用.js.json.node.ms后缀去加载对应文件
    • 如果以上2个条件都不满足,那么尝试对应路径下的index.jsindex.jsonindex.node文件
  • 如果以上2个方法都没有找到对应文件路径,那么就对文件路径后添加分别添加.js.json.node.ms后缀去加载对应的文件(对应源码当中的方法为tryExtensions)

  1. require模块的时候,传入的字符串最后一个字符是/时,即require的是一个文件夹时:
  • 首先查询这个文件夹下的package.json文件中的main字段对应的路径,具体的流程方法和上面说的查找package.json文件的一致
  • 查询当前文件下的index.jsindex.jsonindex.node等文件

当找到文件的路径后就调用tryModuleLoad开始加载模块了,这个方法内部实际上是调用了模块实例的load方法:

Module.prototype.load = function () {

  ...
  this.filename = filename;
  // 定义module的paths。获取这个module路径上所有可能的node_modules路径
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  // 开始load这个文件
  Module._extensions[extension](this, filename);
  this.loaded = true;

  ...
}

调用Module._extension方法去加载不同格式的文件,就拿js文件来说:

Module._extensions['.js'] = function(module, filename) {
  // 首先读取文件的文本内容
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(internalModule.stripBOM(content), filename);
};

内部调用了Module.prototype._compile这个方法:

Module.prototype._compile = function (content, filename)) {
  content = internalModule.stripShebang(content);

  // create wrapper function
  // 将源码的文本包裹一层
  var wrapper = Module.wrap(content);

  // vm.runInThisContext在一个v8的虚拟机内部执行wrapper后的代码
  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });

  var inspectorWrapper = null;
  if (process._breakFirstLine && process._eval == null) {
    if (!resolvedArgv) {
      // we enter the repl if we're not given a filename argument.
      if (process.argv[1]) {
        resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
      } else {
        resolvedArgv = 'repl';
      }
    }

    // Set breakpoint on module start
    if (filename === resolvedArgv) {
      delete process._breakFirstLine;
      inspectorWrapper = process.binding('inspector').callAndPauseOnStart;
    }
  }
  var dirname = path.dirname(filename);
  // 构造require函数
  var require = internalModule.makeRequireFunction(this);
  var depth = internalModule.requireDepth;
  if (depth === 0) stat.cache = new Map();
  var result;
  if (inspectorWrapper) {
    result = inspectorWrapper(compiledWrapper, this.exports, this.exports,
                              require, this, filename, dirname);
  } else {
    // 开始执行这个函数
    // 传入的参数依次是 module.exports / require / module / filename / dirname
    result = compiledWrapper.call(this.exports, this.exports, require, this,
                                  filename, dirname);
  }
  if (depth === 0) stat.cache = null;
  return result;
}

Module.wrap = function(script) {
  return Module.wrapper[0] + script + Module.wrapper[1];
};

Module.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];
  • 通过Module.wrap将源码包裹一层(遵循commonJS规范)
  • 通过调用vmv8虚拟机暴露出来的方法来构造一个新的函数
  • 完成函数的调用

通过源码发现,Module.wrapper在对源码文本进行包裹的时候,传入了5个参数:

  • exports

是对于第三个参数moduleexports属性的引用

  • require

这个require并非是Module.prototype.require方法,而是通过internalModule.makeRequireFunction重新构造出来的,这个方法内部还是依赖Module.prototype.require方法去加载模块的,同时还对这个require方法做了一些拓展。

  • module

module对象,如果需要向外暴露API供其他模块来使用,需要在module.exports属性上定义

  • __filename

当前文件的绝对路径

  • __dirname

当前文件的父文件夹的绝对路径

几个问题

exports 和 module.exports的关系

特别注意第一个参数和第三参数的联系:第一参数是对于第三个参数的exports属性的引用。一旦将某个模块exports赋值给另外一个新的对象,那么就断开了exports属性和module.exports之间的引用关系,同时在其他模块当中也无法引用在当前模块中通过exports暴露出去的API,对于模块的引用始终是获取module.exports属性。

循环引用

官方示例:

a.js

console.log('a 开始');
exports.done = false;
const b = require('./b.js');
console.log('在 a 中,b.done = %j', b.done);
exports.done = true;
console.log('a 结束');

b.js

console.log('b 开始');
exports.done = false;
const a = require('./a.js');
console.log('在 b 中,a.done = %j', a.done);
exports.done = true;
console.log('b 结束');

main.js

console.log('main 开始');
const a = require('./a.js');
const b = require('./b.js');
console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);
$ node main.js
main 开始
a 开始
b 开始
在 b 中,a.done = false
b 结束
在 a 中,b.done = true
a 结束
在 main 中,a.done=true,b.done=true

a模块加载时,需要加载b模块,但是在实际加载a模块之前,就已经将a模块进行的缓存,具体参见Module._load方法:

Module._cache[filename] = module;

tryModuleLoad(module, filename);

因为在加载b模块的过程中再次去加载a模块的时候,这时是直接从缓存中获取a模块导出的API,此时exports.done的属性还是false,未被设置为true,只有当b模块被完全加载后,a模块exports属性才被设置为true