引言
首先說一下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的模塊化的特點也就都有很明確的解釋了。