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

看Node.js中的JavaScript参考

时间:2023-03-20 21:05:17 科技观察

早期(2011-2012)学习Node.js的时候,很多都是从PHP转过来的。当时有人表示Node.js编辑完代码需要重启麻烦(PHP不需要这个过程),于是社区里的朋友开始提倡使用node-supervisor模块来启动项目,这修改代码后可以自动重启。不过相比PHP还是不够方便,因为重启Node.js后,之前的上下文就丢失了。虽然可以通过将session数据保存在数据库或者缓存中来减少重启时的数据丢失,但是如果是在production中,在更新代码的重启间隔期间无法处理请求(PHP可以,而那时候的Node.js还没有集群)。由于这个问题,我从PHP转到了Node.js,我开始思考是否有一种方法可以在不重启的情况下热更新Node.js代码。起初,我把目光投向了require模块。思路很简单,因为Node.js引入的一个模块就是通过require方法加载的。于是我开始思考require是否可以在更新代码后重新require一下。尝试以下操作:a.jsvarexpress=require('express');varb=require('./b.js');varapp=express();app.get('/',function(req,res){b=require('./b.js');res.send(b.num);});app.listen(3000);b.jsexports.num=1024;两个js文件写完后,从a.js开始,刷新页面在b.js中输出1024,然后修改b.js文件中导出的值,比如修改为2048,再次刷新页面还是这样原来1024。再次执行require没有刷新代码。require在执行过程中加载代码后,会将模块导出的数据放在require.cache中。require.cache是??一个{}对象,以模块的绝对路径为key,以模块的详细数据为value。所以我开始尝试以下方法:a.jsvarpath=require('path');varexpress=require('express');varb=require('./b.js');varapp=express();app.get('/',function(req,res){if(true){//检查文件是否被修改flush();}res.send(b.num);});functionflush(){deleterequire.cache[path.join(__dirname,'./b.js')];b=require('./b.js');}app.listen(3000);再次require之前,先把require放在这个模块缓存清空后,再用之前的方法测试一下。结果发现b.js的代码可以刷新成功,可以输出新修改的值。理解了这一点之后,我想利用这个原理实现一个不重启的热更新版node-supervisor。在封装模块的过程中,出于感情原因,考虑在PHP中提供一个类似include的功能来代替require引入一个模块。实际内部还是使用require来加载。以b.js为例,将原来的写法改为varb=include(‘./b’)。b.js文件更新后,include里面可以自动刷新,这样外面就可以得到最新的代码了。但是在实际的开发过程中,这很快就遇到了问题。我们想要的代码可能如下所示:web.jsvarinclude=require('./include');varexpress=require('express');varb=include('./b.js');varapp=express();app.get('/',function(req,res){res.send(b.num);});app.listen(3000);但是在按照这个目标封装include的时候,我们发现了一个问题。无论我们在include.js中做什么,我们都无法像开始时那样获得新的b.num。对比一开始的代码,发现问题出在缺少b=xx。也就是说可以这么写:web.jsvarinclude=require('./include');varexpress=require('快递');varapp=express();app.get('/',function(req,res){varb=include('./b.js');res.send(b.num);});app.listen(3000);这样修改,可以保证每次都可以正确的刷新到最新的代码,而不用重启实例。有兴趣的读者可以研究下这个include是如何实现的。本文不会深入讨论,因为这种技术用的不多,写起来也不是很优雅[1]。相反,还有一个更重要的问题——JavaScript引用。JavaScript引用和传统引用的区别要讨论这个问题,首先要了解JavaScript引用和其他语言的区别。C++中的引用可以直接修改外部值:#includeusingnamespacestd;voidtest(int&p)//passbyreference{p=2048;}intmain(){inta=1024;int&p=a;//将引用p设置为test(p);//调用函数cout<<"p:"<data是通过obj.datadata.name='Alan';data.test=function(){console.log('hi')};//可以直接通过data值console修改为data.log(obj)//{data:{name:'Alan',test:[Function]}}data={name:'Bob',add:function(a,b){returna+b;}};//data是一个引用,直接赋值,只是让这个变量等于另一个引用,不会修改obj本身console.log(data);//{name:'Bob',add:[Function]}console.log(obj);//{data:{name:'Alan',test:[Function]}}obj.data={name:'Bob',add:function(a,b){returna+b;}}};//只有通过obj.data我们才能真正修改数据本身console.log(obj);//{data:{name:'Bob',add:[Function]}}通过这个例子我们可以看到,虽然data像引用一样指向obj.data,但是通过data可以访问到obj.data上的属性。但是由于JavaScript传值的特点,直接修改data=xxx不会使obj.data=xxx。比如最初设置vardata=obj.data时,内存中的情况大概是:|地址|内容||------------|--------|对象数据|内存1||数据|memory1|所以obj.data的memory1可以通过data.xx修改。然后设置data=xxx,因为data是复制的一个新值,但是这个值是一个引用(指向内存1)。使它等于另一个对象就像:|地址|内容||------------|--------|对象数据|内存1||数据|memory2|letdata指向一块新的内存2,如果是传统的引用(比如上面提到的C++引用),那么obj.data本身就会成为一块新的内存2,但是JavaScript都是传值的,而对象在传输过程中复制一个新的引用。所以这个新复制的变量在不影响原始对象的情况下发生了变化。Node.js中module.exports和exports的关系上面例子中obj.data和data的关系就是Node.js中module.exports和exports的关系。让我们看一下Node.js中require文件的实际结构:functionrequire(...){varmodule={exports:{}};((module,exports)=>{//在Node.js文件外面其实包裹了一层自执行函数//这是你模块里面的代码functionsome_func(){};exports=some_func;//有了这个赋值,exports将不再指向module.exports//并且module.exports仍然是{}module.exports=some_func;//这个设置可以修改为原来的exports})(module,module.exports);returnmodule.exports;}所以很自然:console.log(module.exports===exports);//true//所以exports操作的是module.exports。Node.js中的导出是对module.exports副本的引用。exports可以用来修改当前Node.js文件导出的属性,但是不能修改当前模块本身。只能通过module.exports修改为自身。性能方面:exports=1;//无效的module.exports=1;//valid这是两者在性能上的区别,其他方面没有区别。所以你现在应该知道写module.exports.xx=xxx;的人了。实际上写了一个额外的模块。一个更复杂的例子为了进一步练习,让我们看一个更复杂的例子:vara={n:1};变量b=a;a.x=a={n:2};控制台日志(a.x);控制台日志(b.x);根据初步结论,我们可以一步步来看这个问题:vara={n:1};//引用a指向内存1{n:1}varb=a;//引用b=>a=>{n:1}内部结构:|地址|内容||--------|------------||一个|内存1{n:1}||乙|内存1|继续阅读:a.x=a={n:2};//(memory1insteadofa).x=referencea=memory2{n:2}a虽然是引用,但是JavaScript是这个引用传值,所以修改了不影响原来的地方。|地址|内容||------------|--------------------||1)一个|内存2({n:2})||2)内存1.x|记忆2({n:2})||3)乙|内存1({n:1,x:内存2})|所以***结果a.x即(memory2).x==>{n:2}.x==>undefinedb.x即(memory1).x==>memory2==>{n:2}SummaryNoJavaScript中的引用不传递,只有值传递。一个对象(引用类型)的传递只是一个新引用的拷贝。这个新的引用可以访问原来对象上的属性,但是新的引用本身是一个放在另一个格子上的值,新的值是直接赋值给这个格子的,而不影响原始对象。本文开头讨论的Node.js热更新也遇到了这个问题。不同的是对象本身发生了变化,复制的引用仍然指向旧的内存,所以不能通过旧的引用调用新的方法。Node.js并没有对JavaScript施什么黑魔法,引用问题仍然是JavaScript内容。例如,module.exports和exports隐藏了一些细节,很容易被误解。本质还是JavaScript的问题。另外推荐一个关于Node.js的进阶教程《Node.js 面试》。注[1]:说实话,函数中模块的声明有点谭浩强的感觉。把b=include(xxx)写在调用里面,也可以写在public的地方,设置成中间件绑定。除了在调用里面写,还可以导出一个工厂函数,每次用到的时候调用b().num。也可以以中间件的形式绑定到框架的公共对象上(eg:ctx.b=include(xxx))。要实现这样的热更新,架构必须严格避免旧代码被引用的可能性,否则很容易写出内存泄漏的代码。