引言
首先说一下CommonJS 模块和ES6模块二者的区别,这里就直接先直接给出二者的差异。
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块
commonJS模块化的源码解析
首先是nodejs的模块封装器
(function(exports, require, module, __filename, __dirname) {
// 模块的代码实际上在这里
});
以下是node的Module的源码
先看一下我们require一个文件会做什么
Module.prototype.require = function(id) {
validateString(id, 'id');
requireDepth++;
try {
return Module._load(id, this, /* isMain */ false);
} finally {
requireDepth--;
}
};
走到这里至少就佐证了CommonJS 模块是运行时加载,因为require实际就是module这个对象的一个方法,所以require一个js的模块,必须是得在运行到某个module的require代码时才能去加载另一个文件。
然后这里指向了_load方法 这里有个细节就是isMain这个的话其实就是node去区分加载模块是否是主模块的,因为是require的所以必然不应该是一个主模块,当时循环引用的场景除外。
Module.prototype._load
接下来就找到_load方法
Module._load = function(request, parent, isMain) {
let relResolveCacheIdentifier;
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
const mod = loadNativeModule(filename, request, experimentalModules);
if (mod && mod.canBeRequiredByUsers) return mod.exports;
// Don't call updateChildren(), Module constructor already does.
const module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
Module._cache[filename] = module;
if (parent !== undefined) {
relativeResolveCache[relResolveCacheIdentifier] = filename;
}
let threw = true;
try {
module.load(filename);
threw = false;
} finally {
if (threw) {
delete Module._cache[filename];
if (parent !== undefined) {
delete relativeResolveCache[relResolveCacheIdentifier];
}
}
}
return module.exports;
};
首先看一下cache,它实际上是处理了多次require的情况,从源码中可以发现多次require一个模块,node永远的使用了第一次的module对象,并未做更新)。cache的细节其实和node模块输出的变量为什么不能在运行时被改变也是有关系的。因为就算运行中去改变某个模块输出的变量,然后在另一个地方再次require,可是此时module.exports由于有cache,所以并不会发生变化。但是这里还不能说明CommonJS 模块输出的是一个值的拷贝。
接着来看new Module(filename, parent)实例化后运行的module.load
核心我们关注的代码就是
Module._extensions[extension](this, filename);
这里是加载的代码,然后我们看一下js文件的加载
Module._extensions['.js'] = function(module, filename) {
。。。
const content = fs.readFileSync(filename, 'utf8');
module._compile(content, filename);
};
Module.prototype._compile
这里就是我们编写的js文件被加载的过程。
以下的代码经过大量删减
Module.prototype._compile = function(content, filename) {
const compiledWrapper = wrapSafe(filename, content, this);
const dirname = path.dirname(filename);
const require = makeRequireFunction(this, redirects);
var result;
const exports = this.exports;
const thisValue = exports;
const module = this;
result = compiledWrapper.call(thisValue, exports, require, module,
filename, dirname);
return result;
};
首先
const require = makeRequireFunction(this, redirects);
这个是代码中实际require关键词是如何工作的关键。没有什么复杂的,主要是如何针对入参去找文件的,这里就跳过了详细的建议看一下node的官方文档。这里就是’CommonJS 模块是运行时加载‘的铁证,因为require其实都依赖于node模块的执行时的注入,内部require的module更加需要在运行时才会被compile了。
另外注意到this.exports作为参数传递到了wrapSafe中,而整个执行作用域锁定在了this.exports这个对象上。这里是’CommonJS 模块的顶层this指向当前模块‘这句话的来源。
再看一下核心的模块形成的函数wrapSafe
function wrapSafe(filename, content, cjsModuleInstance) {
...
let compiled;
try {
compiled = compileFunction(
content,
filename,
0,
0,
undefined,
false,
undefined,
[],
[
'exports',
'require',
'module',
'__filename',
'__dirname',
]
);
} catch (err) {
...
}
return compiled.function;
}
核心代码可以说非常少,也就是一个闭包的构造器。也就是文章开头提到的模块封装器。
compileFunction。这个接口可以看node对应的[api](http://nodejs.cn/api/vm.html#...
)。
再看一个node官方对require的整个的简化版
function require(/* ... */) {
//对应new Module中 this.export = {}
const module = { exports: {} };
//这里的代码就是对应了_load里的module.load()
((module, exports) => {
// Module code here. In this example, define a function.
function someFunc() {}
exports = someFunc;
// At this point, exports is no longer a shortcut to module.exports, and
// this module will still export an empty default object.
module.exports = someFunc;
// At this point, the module will now export someFunc, instead of the
// default object.
})(module, module.exports);
//注意看_load最后的输出
return module.exports;
}
这个时候再比较一下_load的代码是不是恍然大悟。
最后就是’CommonJS 模块输出的是一个值的拷贝‘的解释了,在cache的机制中已经说明了为什么重复require永远不会重复执行,而在上面的函数中可以看到我们使用的exports中的值的拷贝。
到这里整个node的模块化的特点也就都有很明确的解释了。