理解JS中的Module


require函数

Node.js采用CommonJS规范,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

//module1.js
function greet(){
    consolo.log('greet!')
}
module.exports = greet;

//app.js
var greet = require('./module1.js');
greet(); //greet!

上面例子中,module.exports返回的是一个函数对象

module对象

每个JS文件内部都有一个module对象,代表当前模块,module它的原型是Module。猜想module对象创建的过程为:

  1. 当编译require时,找到.js文件,Module会为其生成一个id并注册到一个全局对象中维护
  2. 创建module对象和该id绑定
  3. 编译该.js文件时,从id索引到module对象

我们先写一个空的module,观察其所有属性:

console.log(module);

/*
Module {
  id: '.',
  exports: {},
  parent: null,
  filename: '/home/user/path/to/module.js',
  loaded: false,
  children: [],
  paths:[...]
}
*/

各属性其含义如下: (1) module.id: 模块的识别符,通常是带有绝对路径的模块文件名。 (2) module.filename 模块的文件名,带有绝对路径。 (3) module.loaded 返回一个布尔值,表示模块是否已经完成加载。 (4) module.parent 返回一个对象,表示调用该模块的模块,当该模块被require时,parent指向require该模块的模块 (5) module.children 返回一个数组,表示该模块要用到的其他模块。例如,当前模块require了3个其它模块,那么children数组的值为这3个模块对象 (6) module.exports 表示模块对外输出的值。

Require Behind Sence

有了前面的铺垫,接下来我们可以分析Node.js中实现require函数的源码

//module1.js
function greet(){
    consolo.log('greet!')
}
module.exports = greet;

//app.js
var greet = require('./module1.js');
greet(); 

接下来从app.js的第一句开始,前面已经提到require并非是一个编译器的预处理符号,而是一个函数,需要被解释器解释执行,可以在require处打一个断点,观察Nodejs的执行过程。

> var greet = require('./module1');
  greet();

断点后,首先进入内部的require函数,传入path为当前文件的路径

  function require(path) {
    try {
      exports.requireDepth += 1;
      return mod.require(path); //返回mod.require,path是当前文件路径
    } finally {
      exports.requireDepth -= 1;
    }
  }

我们最终得到的var greet就是mod.require(path);这个函数的返回值,继续看看这个函数干了什么事情

// Loads a module at the given file path. Returns that module's
// `exports` property.
Module.prototype.require = function(path) {
  assert(path, 'missing path');
  assert(typeof path === 'string', 'path must be a string');
  return Module._load(path, this, /* isMain */ false);
};

从这个函数的注释中可知,该函数会加载path文件中所有的module,并返回它们的exports成员,因此var greet的值间接来自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) {
    //1. check cache
    //2. check native
    //3. create a new module
    //filename : "xxxx/module1.js"
    //parent: "xxxx/app.js"
    var module = new Module(filename, parent);
    //...
    Module._cache[filename] = module;
    tryModuleLoad(module, filename);
    return module.exports;
}

省略掉一些代码,注释上看,这个函数只做三件事,检查缓存中是否有这个module,检查该module是否是NativeModule,所谓NativeModule是Node.js中V8部分的C++代码,显然,这里的module1.js不是NativeModule,于是走到了第三步,创建一个新的JS Module,两个参数分别为module1.js的路径和当前app.js文件的路径。

这个函数很关键,从这里开始,便有了module对象,而在创建完module后,将其放入了全局的Module._cache中,但此时创建的module对象是个空壳,如果访问module.exports返回的是一个空对象,因此接下来需要执行load module的操作即tryModuleLoad函数,最后将制作好的module.exports对象返回给var greet对象。

到这里基本流程就走完了,但实际上tryModuleLoad的实现非常复杂,其大概思路为读取module1.js中的代码,调用JavasScript Core的Native代码(V8)进行求值后返回。对此感兴趣的同学可继续阅读附录中的内容,不想了解细节的同学,看到这里也就足够了。

More on Require

如果被require的文件过多,可以将他们放到文件夹中,并创建一个index.js文件用来索引其它被require的文件。假设文件结构如下

├── app.js
├── greet
│   ├── English.js
│   ├── Spanish.js
│   ├── config.json
│   └── index.js

app.js中可以通过require './greet'来间接应用English.jsSpanish.js两个Module,require './greet'会调用默认require './greet/index.js'

//index.js
var e = require ('./English')
var s = require ('./Spanish')
module.exports = {
    english:e,
    spanish:s
}
//app.js
var greet = require('./greet')
greet.english();
greet.spanish();

Native Modules

上面我们分析了require的内部实现,其中在module.load的函数中,我们发现Node.js可以load native的module,其源码为:

Module._load = function(request, parent, isMain) {
  //...
  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }
  ///....
}

因此我们可以尝试在Node.js中require Native的Module,可以到Node.js的文档中找到所有的Native Module,可以随便尝试一个

var util = require('utl');
var name = "Tony";
//看起来像C的print
var greeting = util.format('Hello, %s',name);
util.log(greeting); //10 Mar 01:56:23 - Hello, Tony

需要注意的是,在require native module时,不需要指定路径和后缀名,如果有同名的js module,在require的时候需要加上路径,例如require('path/util')

小结

  1. 所有代码都运行在模块作用域,不会污染全局作用域。
  2. 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  3. 模块加载的顺序,按照其在代码中出现的顺序。

ES6 Syntax

在ES6中,requiremodule.exports变成了exportimport,例如

//module1.js
export function greet(){
    console.log("Greeting!");
}
//app.js
import * as greeter from 'greet';
greeter.greet();

(全文完)

附录 tryLoadModule的实现

tryLoadModule首先会执行module.load函数,如下

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;
  //...
};

extension为Module文件类型,如果没有指定,则默认为.js,另外运行时可以看到Module._extensions可支持的文件类型有三种,.js/.json/.node。每种类型的文件,对应一个解析的API,接下来会执行Module.extensions[.js](this,filename)

// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(internalModule.stripBOM(content), filename);
};

这个函数首先读取了module1.js中的代码,接下来调用module._compile对这部分代码做了一些预处理,具体为

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

NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];

这里很有意思,所谓的module._compile实际上是将module中的代码包装了一层变为了IIFE的形式

(function (exports, require, module, __filename, __dirname){
    function greet(){
        consolo.log('greet!')
    }
    module.exports = greet;
})

接着将包装后的代码丢给 vm.js中的runInThisContext来执行,该函数用来和V8通信,返回一个compilerWrapper的对象,该对象会来执行module1.js中的代码, 即module.exports = greet

//参数this.exports即为module.exports
result = compiledWrapper.call(this.exports, this.exports, require, this,filename, dirname);

//module1.js
function greet(){
    consolo.log('greet!')
}
> module.exports = greet; //执行这一句