当前位置: 首页 > 科技观察

使用了这么久的require,你真的了解它的原理了吗?_0

时间:2023-03-15 19:27:07 科技观察

我们常说node并不是一门新的编程语言,它只是javascript的runtime,runtime可以简单理解为运行javascript的环境。在大多数情况下,我们会在浏览器中运行javascript。随着node的出现,我们可以在node中运行javascript,也就是说只要安装了node或者浏览器,我们就可以运行javascript。1.节点模块化的实现Node有自己的模块化机制,每个文件都是一个单独的模块,遵循CommonJS规范,即使用require导入模块,通过module.export模块导出。node模块的运行机制也很简单。实际上,每个模块都包裹着一层功能。通过函数包装,可以实现代码之间的作用域隔离。你可能会说我写代码的时候没有把函数包装起来。是的,它是真实的。这层功能是node自动帮我们实现的。我们可以测试一下。我们新建一个js文件,第一行打印一个不存在的变量。比如我们这里打印window,但是node里面没有window。控制台日志(窗口);通过node执行文件,会发现报错信息如下。(请使用系统默认的cmd来执行命令)。(函数(导出,要求,模块,__filename,__dirname){console.log(窗口);ReferenceError:windowisnotdefinedatObject.(/Users/choice/Desktop/node/main.js:1:75)atModule._compile(内部/模块/cjs/loader.js:689:30)atObject.Module._extensions..js(internal/modules/cjs/loader.js:700:10)atModule.load(internal/modules/cjs/loader.js:599:32)attryModuleLoad(internal/modules/cjs/loader.js:538:12)atFunction.Module._load(internal/modules/cjs/loader.js:530:3)atFunction.Module.runMain(internal/modules/cjs/loader.js:742:12)atstartup(internal/bootstrap/node.js:279:19)atbootstrapNodeJSCore(internal/bootstrap/node.js:752:3)可以看到报错的顶层有一个自执行的Function,function包含了exports,require,module,__filename,__dirname,这些我们常用的全局变量,我在上篇文章《前端模块化发展历程》介绍过。自执行函数也是其中一种实现方案前端模块化,在早期的前端时代没有模块化系统,自执行函数可以很好的解决命名空间问题,模块依赖的其他模块可以通过参数传入。cmd和amd规范也是依靠自执行函数实现的。在模块系统中,每个文件都是一个模块,在每个模块外自动设置一个函数,定义导出方法module.exports或exports,同时定义导入方法require。letmoduleA=(function(){module.exports=Promise;returnmodule.exports;})();2.require加载模块require依赖node中的fs模块加载模块文件,fs.readFile读取一个字符串。在javascrpt中我们可以使用eval或者newFunction将一个字符串转换成js代码运行。evalconstname='yd';conststr='consta=123;console.log(name)';eval(str);//yd;newFunctionnewFunction接收一个待执行的字符串并返回一个新的函数,调用这个新的函数字符串,它就会被执行。如果这个函数需要传参,可以在newFunction的时候依次传入参数,最后传入要执行的字符串。比如这里传递了参数b,要执行的字符串str。constb=3;conststr='leta=1;returna+b';constfun=newFunction('b',str);console.log(fun(b,str));//4可以看到eval和Function实例化都可以用来执行javascript字符串,貌似都可以用来实现require模块加载。但是,它们并没有被选中来实现节点中的模块化。原因很简单,因为它们都有一个致命的问题,就是容易受到不属于自己的变量的影响。下面的str字符串没有定义a,但是上面定义的a变量确实可以用,显然是错误的。在模块化机制中,str字符串应该有自己独立的运行空间,不允许有自己不存在的变量。可以直接使用。conststr='console.log(a)';eval(str);constfunc=newFunction(str);func();node有一个vm虚拟环境的概念,用来运行额外的js文件,他可以保证javascript执行的独立性,不会受到外界的影响。vm内置模块虽然我们在外部定义了hello,但是str是一个独立的模块,不在villagehello变量中,所以会直接报错。//引入vm模块,无需安装,node自建模块constvm=require('vm');consthello='yd';conststr='console.log(hello)';wm.runInThisContext(str);//报错所以node可以使用vm来实现javascript模块。可以保证模块的独立性。3.require代码实现介绍在require代码实现之前,先回顾一下这两个node模块的用法,因为后面会用到。路径模块用于处理文件路径。basename:基本路径,如果有文件路径,则不是基本路径,基本路径为1.jsextname:获取扩展名dirname:父路径join:加入路径resolve:当前文件夹的绝对路径,注意使用时不要结束添加/__dirname:当前文件所在文件夹的路径__filename:当前文件的绝对路径constpath=require('path','s');console.log(path.basename('1.js'));控制台。日志(path.extname('2.txt'));console.log(path.dirname('2.txt'));console.log(path.join('a/b/c','d/e/f'));//a/b/c/d/e/console.log(path.resolve('2.txt'));fs模块用于对文件或文件夹进行操作,如读写文件、添加、删除等。常用的方法有readFile和readFileSync,分别是异步和同步读取文件。constfs=require('fs');constbuffer=fs.readFileSync('./name.txt','utf8');//如果不传入编码,则输出为二进制console.log(buffer);fs。access:判断文件是否存在,node10提供,exists方法已经废弃,原因是不符合node规范,所以我们使用access判断文件是否存在。try{fs.accessSync('./name.txt');}catch(e){//文件不存在}4.手动实现require模块加载器先导入依赖模块路径,fs,vm,创建一个RequireFunction,这个函数接收一个modulePath参数,表示要导入的文件路径。//导入依赖constpath=require('path');//路径操作constfs=require('fs');//文件读取constvm=require('vm');//文件执行//定义导入类,参数为模块路径functionRequire(modulePath){...}获取模块在Require中的绝对路径,方便使用fs加载模块。这里我们使用newModule对模块内容进行抽象,使用tryModuleLoad加载模块内容。Moduleand我们后面会实现tryModuleLoad,require的返回值应该是模块的内容,也就是module.exports。//定义导入类,参数为模块路径=newModule(absPathname);//加载当前模块tryModuleLoad(module);//返回exports对象returnmodule.exports;}Module的实现很简单,为模块创建一个exports对象,在tryModuleLoad执行的时候将内容添加到exports中,id是模块路径的绝对名称。//定义模块,添加文件id标识和exports属性functionModule(id){this.id=id;//读取的文件内容会放在exports中this.exports={};}我们之前说过节点模块它运行在一个函数中。这里我们将静态属性包装器挂载到模块中,并在其中定义了这个函数的字符串。包装器是一个数组。数组的第一个元素是函数的参数部分,包括exports、module。require,__dirname,__filename都是我们模块中常用的全局变量。注意这里传入的Require参数是我们自己定义的Require。第二个参数是函数的结尾。这两部分都是字符串,我们在使用时只需要将它们包裹在模块的字符串之外即可。module.wrapper=["(function(exports,module,Require,__dirname,__filename){","})"]_extensions用于对不同的模块扩展使用不同的加载方式,比如json和javascript的加载方式必须不同.JSON使用JSON.parse来工作。JavaScript使用vm.runInThisContext来运行。可以看到fs.readFileSync传入了module.id,也就是我们在定义Module的时候,id里面存放的是模块的绝对路径,读取的内容是一个字符串。我们使用模块。用wrapper将其包裹起来,相当于在模块外包裹了另外一个函数,同样实现了privatescope。使用call执行fn函数。第一个参数改变runningthis我们传入module.exports,后一个参数是包装参数exports,module,Require,__dirname,__filenameModule._extensions={'.js'(module){constcontent=fs.readFileSync(module.id,'utf8');constfnStr=Module.wrapper[0]+content+Module.wrapper[1];constfn=vm.runInThisContext(fnStr);fn.call(module.exports,module.exports,module,Require,_filename,_dirname);},'.json'(module){constjson=fs.readFileSync(module.id,'utf8');module.exports=JSON.parse(json);//将文件的结果放在exports属性上}}tryModuleLoad函数接收模块对象,通过path.extname获取模块的后缀名,然后使用Module._extensions加载模块。//定义模块加载方法functiontryModuleLoad(module){//获取扩展constexttension=path.extname(module.id);//通过后缀加载当前模块Module._extensions[extension](module);}至此Require加载机制我们差不多讲完了,我们再来看看。Require加载模块时传入模块名,在Require方法中使用path.resolve(__dirname,modulePath)获取文件的绝对路径。然后通过实例化新的Module创建一个模块对象,将模块的绝对路径存储在模块的id属性中,并在模块中创建一个exports属性作为json对象。使用tryModuleLoad方法加载模块,在tryModuleLoad中使用path.extname获取文件的扩展名,然后根据扩展名执行相应的模块加载机制。最终将加载到mountsmodule.exports中的模块。tryModuleLoad执行后,module.exports已经存在,直接return即可。//导入依赖constpath=require('path');//路径操作constfs=require('fs');//文件读取constvm=require('vm');//文件执行//定义导入类,参数为模块路径functionRequire(modulePath){//获取要加载的绝对路径letabsPathname=path.resolve(__dirname,modulePath);//创建模块并新建Module实例constmodule=newModule(absPathname);//加载当前模块tryModuleLoad(module);//返回exports对象returnmodule.exports;}//定义模块,添加文件id标识和exports属性functionModule(id){this.id=id;//读取文件内容会放在exports中this.exports={};}//定义包装模块内容的函数Module.wrapper=["(function(exports,module,Require,__dirname,__filename){","})"]//定义扩展,不同的扩展,加载方式不同,实现js和jsonModule._extensions={'.js'(module){constcontent=fs.readFileSync(module.id,'utf8');constfnStr=模块。wrapper[0]+content+Module.wrapper[1];constfn=vm.runInThisContext(fnStr);fn.call(module.exports,module.exports,module,Require,_filename,_dirname);},'.json'(module){constjson=fs.readFileSync(module.id,'utf8');module.exports=JSON.parse(json);//将文件的结果放在exports属性上}}//定义模块加载方法functiontryModuleLoad(module){//获取展开namecontexttension=path.extname(module.id);//通过后缀加载当前模块Module._extensions[extension](module);}5.给模块添加缓存也比较简单,就是当文件加载完毕,将文件放入缓存中,然后加载模块,查看缓存中是否存在。如果存在,直接使用。如果不存在,再去加爱,加载完再放入缓存。//定义导入类,参数为模块路径functionRequire(modulePath){//获取当前要加载的绝对路径letabsPathname=path.resolve(__dirname,modulePath);//从缓存中读取,如果存在,直接返回结果if(Module._cache[absPathname]){returnModule._cache[absPathname]){returnModule._cache[absPathname].exports;}//尝试加载当前模块tryModuleLoad(module);//创建一个module并创建一个新的Module实例constmodule=newModule(absPathname);//添加缓存Module._cache[absPathname]=module;//加载当前模块tryModuleLoad(module);//返回exports对象returnmodule.exports;}6、自动补全路径自动为模块添加后缀,实现加载无后缀的模块。其实如果文件没有后缀,遍历所有的后缀,检查文件是否存在。//定义导入类,参数为模块路径(Module._extensions);letindex=0;//存储原始文件路径constoldPath=absPathname;functionfindExt(absPathname){if(index===extNames.length){returnthrownewError('文件不存在');}try{fs.accessSync(absPathname);returnabsPathname;}catch(e){constext=extNames[index++];findExt(oldPath+ext);}}//递归追加后缀判断文件是否存在absPathname=findExt(absPathname);//从缓存中读取,如果存在则直接返回结果if(Module._cache[absPathname]){returnModule._cache[absPathname].exports;}//尝试加载当前模块//创建一个模块并新建一个Module实例constmodule=newModule(absPathname);//添加缓存Module._cache[absPathname]=module;//加载当前模块tryModuleLoad(module);//返回exports对象returnmodule.exports;}7.分析及实现步骤导入相关模块,创建Require方法。提取用于通过Module._load方法加载模块。Module.resolveFilename根据相对路径转换为绝对路径。缓存模块Module._cache,不要重复加载同一个模块,提高性能。createmoduleid:保存的内容是exports={}相当于这个。使用tryModuleLoad(module,filename)尝试加载模块。Module._extensions使用读取的文件。module.wrap:将读取的js包装成一个函数。使用runInThisContext运行获取的字符串。让字符串执行并使其适应导出。