[NodeJS源码探秘]之require()
文章目录
部分源码已过时 -> 升级至9.11
NodeJS是时下非常流行的服务器语言, 这个系列将着重研究NodeJS的源码,以期为之做出贡献。
第一篇文章就是要搞清楚我们经常使用的require()函数到底是如何运作的。
概述
require大概是我在NodeJS最经常使用的函数了。在使用它的过程中,我对它到底是如何运作的感到非常好奇:
- require函数看起来像是一个全局变量,那么它的确是吗?
- 为什么通过require函数所调用的JS文件不会污染当前文件?
- NodeJS的模块(Module)系统和require的关系又是如何?
简单的说,require函数是一个闭包的返回值,此闭包封装了此require函数所在的模块(Module)实例。进一步说,我们所使用的require函数是对Module.require的简单封装。后文会对Module模块做出更详细的解释。
require同时还拥有以下属性:resolve, main, extension以及cache。后文会介绍这几个属性的作用。
请允许我直接给出源码中对require的定义:
function makeRequireFunction(mod) {
  const Module = mod.constructor;
  function require(path) {
    try {
      exports.requireDepth += 1;
      return mod.require(path);
    } finally {
      exports.requireDepth -= 1;
    }
  }
  function resolve(request) {
    return Module._resolveFilename(request, mod);
  }
  require.resolve = resolve;
  require.main = process.mainModule;
  // Enable support to add extra extension types.
  require.extensions = Module._extensions;
  require.cache = Module._cache;
  return require;
}
可以看到,require是闭包makeRequireFunction的返回值。那问题在于,此闭包它什么时候被执行呢?返回的require又是怎么提供给我们的JS代码的呢?
假设我们有一个非常简单的a.js如下:
# a.js
const B = require('b.js');
然后我们用node运行此文件:
node a.js
此时,node会做以下几个事情:
- 生成一个Module实例,此实例可以看成是a.js这个文件的抽象。
- 读取a.js的文件内容
- 将a.js封装在一个匿名函数中
- 执行此匿名函数
我们的require函数就是在步骤2和步骤3之间被生成的, 同时在步骤4时传入3中的匿名函数。可以在源码lib/module.js中看到这些步骤的部分细节:
// 这里的content就是a.js中的代码
Module.prototype._compile = function(content, filename) {
  content = internalModule.stripShebang(content);
  // Module.wrap将content封装在下面这个匿名函数里
  // (function (exports, require, module, __filename, __dirname) { 
  //    ...a.js的代码会被注入到这里
  // })
  var wrapper = Module.wrap(content);
  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });
  
  // ....
  var dirname = path.dirname(filename); // 这是我们常用的__dirname变量
  var require = internalModule.makeRequireFunction(this); // 这是require
  var depth = internalModule.requireDepth;
  if (depth === 0) stat.cache = new Map();
  var result;
  if (//...)
    // ...
  } else {
    // 这里执行了我们上面得到的闭包
    result = compiledWrapper.call(this.exports, this.exports, require, this,
                                  filename, dirname);
  }
  if (depth === 0) stat.cache = null;
  return result;
}
通过上述代码,我们可以看到,require函数其实是我们的JS代码执行时所在的函数作用域的一个参数,所以我们才可以直接使用require。
那么require到底会做什么事情呢?
在此之前,我们要来熟悉一下lib/module.js中定义的类,Module。
Module类
lib/module.js定义了一个类,Module。我们用node引入的任何JS文件,在最后都是一个Module实例的一部分。Module类定义了许多变量和函数,比较重要的如下:
Module的静态变量
- Module._cache:
 对象,其值均为Module实例的缓存
- Module._extension:
 对象,其值均为函数,用于加载属于特定文件格式的文件,其中就包括加载JS文件的函数
Module的静态函数
- Module.load():
 当一个模块要加载另一个模块时,会先通过调用此函数查看是否已经对应模块的缓存
- Module.wrapper():
 封装我们用户代码的函数
Module的公有变量
- exports:
 暴露给其他模块的对象
Module的公有函数
- Module.prototype.load():
 输入一个文件名,load函数会根据文件的格式使用对应的Module._extension加载此文件。
- Module.prototype.require():
 这个就是著名的require()函数的真身了,我们将在之后研究单独的require()是如何与Module.prototype.require联系在一起的。
- Module.prototype._compile(): 
 load()函数在加载JS文件的过程中,会调用此函数来封装加载的JS文件,使其有独立的作用域。
require()后都发生了什么?
调用Module._load()
当a.js被node加载后,a.js其实就是一个Module的实例了,所以相应的我们可以在代码中调用require。实际上,我们调用的是Module.prototype.require。而Module.prototype.require又是简单得封装了Module._load()
Module._load()函数会做以下三种事情,这里直接贴上源代码中对其的注释:
// 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.require()` with the
//    filename and return the result.
// 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.
Module._load = function(request, parent, isMain) {
}
在此我就不讨论前两种情况了,因为重要的还是第三点,即加载一个未缓存的JS文件的流程。
调用Module.prototype.load()
因为b.js是第一次被加载,所以Module.prototype._load会先实例化一个Module, 并调用此实例的load()函数。依旧还是贴上代码:
Module._load = function(request, parent, isMain) {
  // 1. 有缓存否
  // 2. 是NativeModule否
  // 3. 我们所要研究的重点
  // filename即我们所要加载的文件b.js
  // parent则是请求加载此文件的Module实例,在我们的预设下,parent为a.js所对应的实例
  var module = new Module(filename, parent);
  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }
  Module._cache[filename] = module;
  // tryModuleLoad其实调用的就是module.load()
  tryModuleLoad(module, filename);
  return module.exports;
}
正如代码所示,最终实例后的b.js其实只是一个空壳,因为b.js中的代码还未被编译,这也是tryModuleLoad所要完成的任务。tryModuleLoad只是try & catch了module.load()函数。而module.load()函数最需要关注的则是它调用了Module._extension中加载JS文件的函数
Module.prototype.load = function(filename) {
  // ...
  
  var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  Module._extensions[extension](this, filename);
  this.loaded = true;
  // ...
};
Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(internalModule.stripBOM(content), filename);
};
调用Module.prototype._compile()
终于,我们来到了最重要的一步,真正的编译b.js中的代码,这由module._compile实现。
Module.prototype._compile = function(content, filename) {
  content = internalModule.stripShebang(content);
  // create wrapper function
  var wrapper = Module.wrap(content);
  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });
  
  // 到这一步,b.js中的代码都被封装进了 '(function (exports, require, module, __filename, __dirname) { })'中
  // ....
  var dirname = path.dirname(filename); // 这是我们常用的__dirname变量
  var require = internalModule.makeRequireFunction(this); // 这是require
  var depth = internalModule.requireDepth;
  if (depth === 0) stat.cache = new Map();
  var result;
  if (//...)
    // ...
  } else {
    // 这里执行了我们上面得到的闭包
    result = compiledWrapper.call(this.exports, this.exports, require, this,
                                  filename, dirname);
  }
  if (depth === 0) stat.cache = null;
  return result;
}
总结
至此,我们应该明白了require本质上是我们所在的JS文件所引用的变量。但说实话,我们的源码探秘还只是象征性地在源码的边界打了个转。但这是值得的,在这个过程中,我又看到了更多需要探秘的问题,其中就有一个我个人非常好奇的地方,即在_compile的过程中,下面这一段代码底下到底发生了什么:
var compiledWrapper = vm.runInThisContext(wrapper, {
  filename: filename,
  lineOffset: 0,
  displayErrors: true
});
既然函数的名字牵扯到了compile, context, vm这样的字眼,我们的代码会被编译成什么?vm是什么?compiledWrapper到底是怎么样的?
好了,探秘继续,感谢您看到最后,下回见!