首先说一下CommonJS 模块和ES6模块两者的区别,这里就直接先直接给出两者的差别。html
首先是nodejs的模块封装器node
(function(exports, require, module, __filename, __dirname) { // 模块的代码实际上在这里 });
如下是node的Module的源码git
先看一下咱们require一个文件会作什么github
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代码时才能去加载另外一个文件。api
而后这里指向了_load方法 这里有个细节就是isMain这个的话其实就是node去区分加载模块是不是主模块的,由于是require的因此必然不该该是一个主模块,当时循环引用的场景除外。闭包
接下来就找到_load方法app
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文件的加载ui
Module._extensions['.js'] = function(module, filename) { 。。。 const content = fs.readFileSync(filename, 'utf8'); module._compile(content, filename); };
这里就是咱们编写的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的模块化的特色也就都有很明确的解释了。