源码分析
在 lib/module.js 文件中定义了require 方法,require 实际上是调用了 Module._load 方法,其主要的流程如下(node v6.10.0):
Module._load = function(request, parent, isMain) {
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
}
// 找出文件的绝对路径
var filename = Module._resolveFilename(request, parent, isMain);
// 检查是否有缓存,如果有,直接返回缓存,不再次加载文件
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
// 检测是否为内置模块
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
}
// 新建模块实例
var module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.'; // 如果此文件为直接调用,id 设置为 '.'
}
// 将实例缓存
Module._cache[filename] = module;
// 加载编译运行文件
tryModuleLoad(module, filename);
// 返回实例的 exports 属性
return module.exports;
};
在 node官方文档中关于循环加载的例子,a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
运行 a.js 并不会出现加载的死循环, 其主要是因为在 b.js require调用 a.js 的时候, a.js 文件的缓存是存在的,这时候会直接返回缓存中的。实际上 直接运行 a.js 和在其他地方require调用 a.js 都是要运行require 方法, 只是参数不同。在 a.js 调用 b.js 之前,已经创建了 a.js 的module 实例并放入了缓存。
tryModuleLoad 方法主要根据文件类型调用相应的方法, 本文只分析 .js文件的加载运行原理。.js文件的处理函数源码:
Module._extensions['.js'] = function(module, filename) {
// 读取文件的内容,拿到的是字符串
var content = fs.readFileSync(filename, 'utf8');
// 编译运行拿到的字符串
module._compile(internalModule.stripBOM(content), filename);
};
编译的函数是理解 node 模块各种知识关键所在,其部分源码如下:
Module.prototype._compile = function(content, filename) {
// 包装文件的源码, 实际上是拼接字符串
var wrapper = Module.wrap(content);
// 将字符串转变为函数
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});
// 调用函数,传入 this 值和其他参数
var result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);
return result;
};
包装源码的方法在 lib/internal/bootstrap_node.js 文件中, 源码如下:
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
// 最终返回的字符串
'(function (exports, require, module, __filename, __dirname) { 文件源码 ,\n});'
可以看到包装后的字符串实际上就是一个函数样子,vm.runInThisContext 方法会将这个字符串转变为可以运行的函数,最终调用传入的参数则可以解释为何 js 文件中的 this 指向 module.exports, 为何 exports 只是module.exports 的一个引用。
有兴趣的可以调试node 的源码, 对 lib 目录中的 js 模块可以有更深刻的认识。