Node.js中看JavaScript的引用
早期学习Node.js的时候(2011-2012),有挺多是从PHP转过来的,当时有部分人对于Node.js编辑完代码需要重启一下表示麻烦(PHP不需要这个过程),于是社区里的朋友就开始提倡使用node-supervisor这个模块来启动项目,可以编辑完代码之后自动重启。不过相对于PHP而言依旧不够方便,因为Node.js在重启以后,之前的上下文都丢失了。
虽然可以通过将session数据保存在数据库或者缓存中来减少重启过程中的数据丢失,不过如果是在生产的情况下,更新代码的重启间隙是没法处理请求的(PHP可以,另外那个时候Node.js还没有cluster)。由于这方面的问题,加上本人是从PHP转到Node.js的,于是从那时开始思考,有没有办法可以在不重启的情况下热更新Node.js的代码。
最开始把目光瞄向了require这个模块。想法很简单,因为Node.js中引入一个模块都是通过require这个方法加载的。于是就开始思考require能不能在更新代码之后再次require一下。尝试如下:
a.js
varexpress=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.js
exports.num=1024;
两个JS文件写好之后,从a.js启动,刷新页面会输出b.js中的1024,然后修改b.js文件中导出的值,例如修改为2048。再次刷新页面依旧是原本的1024。
再次执行一次require并没有刷新代码。require在执行的过程中加载完代码之后会把模块导出的数据放在require.cache中。require.cache是一个{}对象,以模块的绝对路径为key,该模块的详细数据为value。于是便开始做如下尝试:
a.js
varpath=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之上关于该模块的cache清理掉后,用之前的方法再次测试。结果发现,可以成功的刷新b.js的代码,输出新修改的值。
了解到这个点后,就想通过该原理实现一个无重启热更新版本的node-supervisor。在封装模块的过程中,出于情怀的原因,考虑提供一个类似PHP中include的函数来代替require去引入一个模块。实际内部依旧是使用require去加载。以b.js为例,原本的写法改为varb=include(‘./b'),在文件b.js更新之后include内部可以自动刷新,让外面拿到最新的代码。
但是实际的开发过程中,这样很快就碰到了问题。我们希望的代码可能是这样:
web.js
varinclude=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.js
varinclude=require('./include'); varexpress=require('express'); varapp=express(); app.get('/',function(req,res){ varb=include('./b.js'); res.send(b.num); }); app.listen(3000);
修改成这样,就可以保证每次能可以正确的刷新到最新的代码,并且不用重启实例了。读者有兴趣的可以研究这个include是怎么实现的,本文就不深入讨论了,因为这个技巧使用度不高,写起起来不是很优雅[1],反而这其中有一个更重要的问题——JavaScript的引用。
JavaScript的引用与传统引用的区别
要讨论这个问题,我们首先要了解JavaScript的引用于其他语言中的一个区别,在C++中引用可以直接修改外部的值:
#include usingnamespacestd; voidtest(int&p)//引用传递{ p=2048; } intmain(){ inta=1024; int&p=a;//设置引用p指向a test(p);//调用函数 cout<<"p:"<
而在JavaScript中:
varobj={name:'Alan'}; functiontest1(obj){ obj={hello:'world'};//试图修改外部obj } test1(obj); console.log(obj);//{name:'Alan'}//并没有修改① functiontest2(obj){ obj.name='world';//根据该对象修改其上的属性 } test2(obj); console.log(obj);//{name:'world'}//修改成功②我们发现与C++不同,根据上面代码①可知JavaScript中并没有传递一个引用,而是拷贝了一个新的变量,即值传递。根据②可知拷贝的这个变量是一个可以访问到对象属性的“引用”(与传统的C++的引用不同,下文中提到的JavaScript的引用都是这种特别的引用)。这里需要总结一个绕口的结论:Javascript中均是值传递,对象在传递的过程中是拷贝了一份新的引用。
为了理解这个比较拗口的结论,让我们来看一段代码:
varobj={name:'Alan'}; functiontest1(obj){ obj={hello:'world'};//试图修改外部obj } test1(obj); console.log(obj);//{name:'Alan'}//并没有修改① functiontest2(obj){ obj.name='world';//根据该对象修改其上的属性 } test2(obj); console.log(obj);//{name:'world'}//修改成功②通过这个例子我们可以看到,data虽然像一个引用一样指向了obj.data,并且通过data可以访问到obj.data上的属性。但是由于JavaScript值传递的特性直接修改data=xxx并不会使得obj.data=xxx。
打个比方最初设置vardata=obj.data的时候,内存中的情况大概是:
| Addr | 内容 ||----------|--------|obj.data| 内存1|
|data|内存1|所以通过data.xx可以修改obj.data的内存1。
然后设置data=xxx,由于data是拷贝的一个新的值,只是这个值是一个引用(指向内存1)罢了。让它等于另外一个对象就好比:
| Addr | 内容 ||----------|--------|obj.data| 内存1|
|data|内存2|让data指向了新的一块内存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.exportsNode.js中的exports就是拷贝的一份module.exports的引用。通过exports可以修改Node.js当前文件导出的属性,但是不能修改当前模块本身。通过module.exports才可以修改到其本身。表现上来说:
exports=1;//无效 module.exports=1;//有效这是二者表现上的区别,其他方面用起来都没有差别。所以你现在应该知道写module.exports.xx=xxx;的人其实是多写了一个module.。
更复杂的例子
为了再练习一下,我们在来看一个比较复杂的例子:
vara={n:1}; varb=a; a.x=a={n:2}; console.log(a.x); console.log(b.x);按照开始的结论我们可以一步步的来看这个问题:
vara={n:1};//引用a指向内存1{n:1} varb=a;//引用b=>a=>{n:1}内部结构:
| Addr | 内容 ||---------|-------------|
|a|内存1{n:1}||b|内存1|继续往下看:
a.x=a={n:2};//(内存1而不是a).x=引用a=内存2{n:2}a虽然是引用,但是JavaScript是值传的这个引用,所以被修改不影响原本的地方。
|Addr|内容||-----------|-----------------------|
|1)a|内存2({n:2})||2)内存1.x|内存2({n:2})|
|3)b|内存1({n:1,x:内存2})|所以最后的结果
a.x即(内存2).x==>{n:2}.x==>undefined
b.x即(内存1).x==>内存2==>{n:2}总结
JavaScript中没有引用传递,只有值传递。对象(引用类型)的传递只是拷贝一个新的引用,这个新的引用可以访问原本对象上的属性,但是这个新的引用本身是放在另外一个格子上的值,直接往这个格子赋新的值,并不会影响原本的对象。本文开头所讨论的Node.js热更新时碰到的也是这个问题,区别是对象本身改变了,而原本拷贝出来的引用还指向旧的内存,所以通过旧的引用调用不到新的方法。
Node.js并没有对JavaScript施加黑魔法,其中的引用问题依旧是JavaScript的内容。如module.exports与exports这样隐藏了一些细节容易使人误会,本质还是JavaScript的问题。
注[1]:
老实说,模块在函数内声明有点谭浩强的感觉。
把b=include(xxx)写在调用内部,还可以通过设置成中间件绑定在公共地方来写。
除了写在调用内部,也可以导出一个工厂函数,每次使用时b().num一下调用也可以。
还可以通过中间件的形式绑定在框架的公用对象上(如:ctx.b=include(xxx))。
要实现这样的热更新必须在架构上就要严格避免旧代码被引用的可能性,否则很容易写出内存泄漏的代码。
以上所述是小编给大家介绍的Node.js中看JavaScript的引用,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!