引言
首先说一下 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 的模块化的特点也就都有很明确的解释了。