理解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
对象创建的过程为:
- 当编译
require
时,找到.js
文件,Module
会为其生成一个id
并注册到一个全局对象中维护 - 创建
module
对象和该id
绑定 - 编译该
.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.js
和Spanish.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')
小结
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- 模块加载的顺序,按照其在代码中出现的顺序。
ES6 Syntax
在ES6中,require
和module.exports
变成了export
和import
,例如
//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; //执行这一句