介绍首先说一下CommonJS模块和ES6模块的区别。这里我们就直接给出两者的区别。CommonJS模块输出值的副本,ES6模块输出值的引用。CommonJS模块在运行时加载,而ES6模块是编译时输出接口。ES6模块中,最顶层的this指向undefined;CommonJS模块的顶层this指向当前模块commonJS模块化的源码解析首先是模块的nodejs的模块包装器(function(exports,require,module,__filename,__dirname){//代码为实际上在这里});以下是node的Module的源码。让我们先看看当我们需要一个文件时我们会做什么。Module.prototype.require=function(id){validateString(id,'id');要求深度++;try{returnModule._load(id,this,/*isMain*/false);}最后{requireDepth--;}};走到这里至少证明了CommonJS模块是在运行时加载的,因为require其实是模块对象的一个??方法,所以要require一个js模块,运行某个模块的require代码时,必须加载另一个文件。然后它指向_load方法。这里有一个细节isMain其实就是node来区分加载的模块是否是主模块。因为需要,所以一定不能是主模块,除了当时循环引用的场景。Module.prototype._load接下去就找到_load方法Module._load=function(request,parent,isMain){letrelResolveCacheIdentifier;constcachedModule=Module._cache[文件名];if(cachedModule!==undefined){updateChildren(parent,cachedModule,true);返回cachedModule.exports;}constmod=loadNativeModule(文件名、请求、实验模块);如果(mod&&mod.canBeRequiredByUsers)返回mod.exports;//不要调用updateChildren(),模块构造函数已经调用了。constmodule=newModule(文件名,parent);如果(isMain){process.mainModule=module;module.id='.';}Module._cache[文件名]=模块;if(parent!==undefined){relativeResolveCache[relResolveCacheIdentifier]=文件名;}让throw=true;尝试{module.load(文件名);扔=假;}finally{if(throw){deleteModule._cache[文件名];if(parent!==undefined){删除relativeResolveCache[relResolveCacheIdentifier];}}}返回module.exports;};先看cache,其实是处理多个require的情况。从源码可以发现,多次需要一个模块,node总是使用第一个模块对象,不更新)缓存的细节其实和为什么node模块输出的变量不能在运行。因为即使你在运行过程中改变了某个模块输出的变量,然后在另一个地方再次require,这时候module.exports是不会改变的,因为它有缓存。但这并不意味着CommonJS模块的输出是值的副本。然后看newModule(filename,parent)实例化后运行的module.load核心,我们关注的代码是Module._extensions[extension](this,filename);这里是加载的代码,然后我们看js文件的加载Module._extensions['.js']=function(module,filename){...constcontent=fs.readFileSync(文件名,'utf8');module._compile(content,filename);};Module.prototype._compile这里就是加载我们写的js文件的过程。下面的代码已经大大减少了Module.prototype._compile=function(content,filename){constcompiledWrapper=wrapSafe(filename,content,this);constdirname=path.dirname(文件名);constrequire=makeRequireFunction(this,redirects);变量结果;constexports=this.exports;constthisValue=exports;const模块=这个;结果=compiledWrapper.call(thisValue,exports,require,module,filename,dirname);返回结果;};首先constrequire=makeRequireFunction(this,redirects);这是实际require关键字在代码中如何工作的关键。没有什么复杂的,主要是如何找到输入参数的文件。这里我跳过详细的建议,看node的官方文档。这里就是'CommonJS模块在运行时加载'的铁证,因为require实际上依赖于node模块在执行时的注入,内部的require模块需要在运行时编译。另请注意,this.exports作为参数传递给wrapSafe,并且整个执行范围都锁定在this.exports对象上。这就是短语“CommonJS模块的顶级this指向当前模块”的来源。再看核心模块wrapSafe(filename,content,cjsModuleInstance){...letcompiled;形成的函数wrapSafe函数;try{compiled=compileFunction(content,filename,0,0,undefined,false,undefined,[],['exports','require','module','__filename','__dirname',]);}catch(err){...}returncompiled.function;}核心代码可以说很小,就是一个闭包构造函数。也就是文章开头提到的模块包装器。编译函数。这个接口可以看node对应的[api](http://nodejs.cn/api/vm.html#...)。再看一个节点官方简化版的requirefunctionrequire(/*...*/){//对应newModule={}中的this.exportconstmodule={exports:{}};//这里代码对应_load中的module.load()((module,exports)=>{//这里是模块代码,本例中定义了一个函数。functionsomeFunc(){}exports=someFunc;//在此时,exports不再是module.exports的快捷方式,并且//该模块仍将导出一个空的默认对象。module.exports=someFunc;//此时,该模块现在将导出someFunc,而不是//默认对象。})(module,module.exports);//注意_load最后的输出returnmodule.exports;}这时候对比一下_load的代码,是不是恍然大悟。最后是对‘CommonJS模块的输出是一个值的副本’的解释。在缓存机制中,已经解释了为什么重复的require永远不会重复,而在上面的函数中,我们可以看到我们使用的exports中的值。至此,整个节点的模块化已经解释清楚了。
