因为 Node.js 在重启以往

在 Node.js 中看 JavaScript 的引用

2017/05/05 · JavaScript
· NodeJS

原稿出处: lellansin   

前期学习 Node.js 的时候 (二零一二-二〇一一),有挺多是从 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

var express = require(‘express’); var b = require(‘./b.js’); var app =
express(); app.get(‘/’, function (req, res) { b = require(‘./b.js’);
res.send(b.num); }); app.listen(3000);

1
2
3
4
5
6
7
8
9
10
11
var express = require(‘express’);
var b = require(‘./b.js’);
 
var app = express();
 
app.get(‘/’, function (req, res) {
  b = require(‘./b.js’);
  res.send(b.num);
});
 
app.listen(3000);

b.js

exports.num = 1024;

1
exports.num = 1024;

七个 JS 文件写好之后,从 a.js 运转,刷新页面会输出 b.js 中的
1024,然后修改 b.js 文件中程导弹出的值,举个例子修改为
2048。再一次刷新页面依然是原来的 1024。

再一次试行贰回 require 并从未刷新代码。require
在施行的经过中加载完代码之后会把模块导出的数量放在 require.cache
中。require.cache 是一个 { } 对象,以模块的相对路线为
key,该模块的事无巨细数据为 value。于是便初阶做如下尝试:

a.js

var path = require(‘path’); var express = require(‘express’); var b =
require(‘./b.js’); var app = express(); app.get(‘/’, function (req, res)
{ if (true) { // 检查文件是或不是修改 flush(); } res.send(b.num); });
function flush() { delete require.cache[path.join(__dirname,
‘./b.js’)]; b = require(‘./b.js’); } app.listen(3000);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var path = require(‘path’);
var express = require(‘express’);
var b = require(‘./b.js’);
 
var app = express();
 
app.get(‘/’, function (req, res) {
  if (true) { // 检查文件是否修改
    flush();
  }
  res.send(b.num);
});
 
function flush() {
  delete require.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为例,原来的写法改为 var b =
include(‘./b’),在文书 b.js 更新之后 include
内部能够活动刷新,让外界获得新型的代码。

然则其实的支出进度中,那样连忙就凌驾了难点。大家期望的代码可能是那般:

web.js

var include = require(‘./include’); var express = require(‘express’);
var b = include(‘./b.js’); var app = express(); app.get(‘/’, function
(req, res) { res.send(b.num); }); app.listen(3000);

1
2
3
4
5
6
7
8
9
10
var include = require(‘./include’);
var express = require(‘express’);
var b = include(‘./b.js’);
var app = express();
 
app.get(‘/’, function (req, res) {
  res.send(b.num);
});
 
app.listen(3000);

但根据这几个目的封装include的时候,大家开采了难点。无论大家在include.js内部中怎么样达成,都不能够像起先那样得到新的
b.num。

相比较起来的代码,大家开采难题出在少了 b = xx。也便是说那样写才足以:

web.js

var include = require(‘./include’); var express = require(‘express’);
var app = express(); app.get(‘/’, function (req, res) { var b =
include(‘./b.js’); res.send(b.num); }); app.listen(3000);

1
2
3
4
5
6
7
8
9
10
var include = require(‘./include’);
var express = require(‘express’);
var app = express();
 
app.get(‘/’, function (req, res) {
  var b = include(‘./b.js’);
  res.send(b.num);
});
 
app.listen(3000);

修改成那样,就足以确定保证每一回能得以正确的基础代谢到新型的代码,况且不要重启实例了。读者有意思味的能够研讨这些include是怎么落到实处的,本文就不深切座谈了,因为那个本事使费用不高,写起起来不是很优雅[1],反而那在那之中有一个更主要的难题——JavaScript的援用。

JavaScript 的援用与历史观引用的区分

要研究那个难点,我们先是要打听 JavaScript
的引用于其他语言中的三个分歧,在 C++ 中引用能够平素修改外界的值:

#include using namespace std; void test(int &p) // 援用传递 { p = 2048;
} int main() { int a = 1024; int &p = a; // 设置援用p指向a test(p); //
调用函数 cout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include
 
using namespace std;
 
void test(int &p) // 引用传递
{
    p = 2048;
}
 
int main()
{
    int a = 1024;
    int &p = a; // 设置引用p指向a
 
    test(p); // 调用函数
 
    cout

而在 JavaScript 中:

因为 Node.js 在重启以往。var obj = { name: ‘Alan’ }; function test1(obj) { obj = { hello: ‘world’
}; // 试图修改外界obj } test1(obj); console.log(obj); // { name: ‘Alan’
} // 并从未更动① function test2(obj) { obj.name = ‘world’; //
遵照该指标修改其上的属性 } test2(obj); console.log(obj); // { name:
‘world’ } // 修改成功②

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var obj = { name: ‘Alan’ };
 
function test1(obj) {
  obj = { hello: ‘world’ }; // 试图修改外部obj
}
 
test1(obj);
console.log(obj); // { name: ‘Alan’ } // 并没有修改①
 
function test2(obj) {
  obj.name = ‘world’; // 根据该对象修改其上的属性
}
 
test2(obj);
console.log(obj); // { name: ‘world’ } // 修改成功②

咱俩开采与 C++ 不一致,根据上边代码 ① 可见 JavaScript
中并从未传递一个征引,而是拷贝了二个新的变量,即值传递。依据 ②
可见拷贝的那些变量是贰个能够访问到对象属性的“援引”(与价值观的 C++
的援用分裂,下文中涉嫌的 JavaScript
的援引都以这种特别的引用)。这里要求总计多个绕口的结论:Javascript
中均是值传递,对象在传递的进程中是拷贝了一份新的引用。

为了了然这几个比较生硬的定论,让大家来看一段代码:

因为 Node.js 在重启以往。var obj = { data: {} }; // data 指向 obj.data var data = obj.data;
console.log(data === obj.data); // true–>data所操作的便是obj.data
data.name = ‘Alan’; data.test = function () { console.log(‘hi’) }; //
通过data能够直接改动到data的值 console.log(obj) // { data: { name:
‘Alan’, test: [Function] } } data = { name: ‘Bob’, add: function (a,
b) { return a + b; } }; //
data是一个引用,直接赋值给它,只是让这些变量等于其它三个援用,并不会修改到obj自身console.log(data); // { name: ‘鲍勃’, add: [Function] }
console.log(obj); // { data: { name: ‘Alan’, test: [Function] } }
obj.data = { name: ‘鲍伯’, add: function (a, b) { return a + b; } }; //
而透过obj.data本领真的修改到data本身 console.log(obj); // { data: {
name: ‘鲍伯’, add: [Function] } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var obj = {
  data: {}
};
 
// data 指向 obj.data
var data = obj.data;
 
console.log(data === obj.data); // true–>data所操作的就是obj.data
 
data.name = ‘Alan’;
data.test = function () {
  console.log(‘hi’)
};
 
// 通过data可以直接修改到data的值
console.log(obj) // { data: { name: ‘Alan’, test: [Function] } }
 
data = {
  name: ‘Bob’,
  add: function (a, b) {
    return a + 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) {
    return a + b;
  }
};
 
// 而通过obj.data才能真正修改到data本身
console.log(obj); // { data: { name: ‘Bob’, add: [Function] } }

通过那几个事例大家得以看看,data 即便像叁个援引同样指向了
obj.data,况兼通过 data 能够访谈到 obj.data 上的本性。不过由于
JavaScript 值传递的特点直接修改 data = xxx 并不会使得 obj.data = xxx。

因为 Node.js 在重启以往。因为 Node.js 在重启以往。打个即使最早安装 var data = obj.data 的时候,内部存款和储蓄器中的情形大假若:

| Addr | 内容 | |———-|——– | obj.data | 内存1 | | data | 内存1
|

1
2
3
4
|   Addr   |  内容  |
|———-|——–
| obj.data |  内存1 |
|   data   |  内存1 |

故而通过 data.xx 能够修改 obj.data 的内部存款和储蓄器1。

然后设置 data = xxx,由于 data
是拷贝的一个新的值,只是那个值是一个援引(指向内部存款和储蓄器1)罢了。让它也正是别的一个对象就好比:

| Addr | 内容 | |———-|——– | obj.data | 内存1 | | data | 内存2
|

1
2
3
4
|   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
二个文本时的莫过于协会:

function require(…) { var module = { exports: {} }; ((module, exports)
=> { // Node.js 汉语件外部其实被包了一层自进行的函数 //
那中档是你模块内部的代码. function some_因为 Node.js 在重启以往。func() {}; exports =
some_func; // 那样赋值,exports便不再指向module.exports //
而module.exports依然是{} module.exports = some_func; //
那样设置能力修改到原本的exports })(module, module.exports); return
module.exports; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function require(…) {
  var module = { exports: {} };
  ((module, exports) => { // Node.js 中文件外部其实被包了一层自执行的函数
    // 这中间是你模块内部的代码.
    function some_func() {};
    exports = some_func;
    // 这样赋值,exports便不再指向module.exports
    // 而module.exports依旧是{}
 
    module.exports = some_func;
    // 这样设置才能修改到原本的exports
  })(module, module.exports);
  return module.exports;
}

由此很自然的:

console.log(module.exports === exports); // true // 所以 exports
所操作的正是 module.exports

1
2
console.log(module.exports === exports); // true
// 所以 exports 所操作的就是 module.exports

Node.js 中的 exports 正是拷贝的一份 module.exports 的援用。通过 exports
能够修改Node.js 当前文件导出的属性,可是不能够改改当前模块自身。通过
module.exports 才方可修改到其本身。表现上来讲:

exports = 1; // 无效 module.exports = 1; // 有效

1
2
exports = 1; // 无效
module.exports = 1; // 有效

那是双方展现上的差异,其余地点用起来都未曾差距。所以你以后理应知道写module.exports.xx
= xxx; 的人其实是多写了三个module.。

更复杂的例子

为了再演练一下,大家在来看八个比较复杂的例子:

var a = {n: 1}; var b = a; a.x = a = {n: 2}; console.log(a.x);
console.log(b.x);

1
2
3
4
5
var a = {n: 1};  
var b = a;
a.x = a = {n: 2};  
console.log(a.x);
console.log(b.x);

依据初阶的定论我们得以一步步的来看这一个标题:

var a = {n: 1}; // 援用a指向内部存款和储蓄器1{n:1} var b = a; // 援用b => a =>
{ n:1 }

1
2
var a = {n: 1};   // 引用a指向内存1{n:1}
var b = a;        // 引用b => a => { n:1 }

内部结构:

| Addr | 内容 | |———|————-| | a | 内存1 {n:1} | | b |
内存1 |

1
2
3
4
|   Addr  |     内容     |
|———|————-|
|    a    |  内存1 {n:1} |
|    b    |  内存1       |

后续往下看:

a.x = a = {n: 2}; // (内存1 而不是 a ).x = 引用 a = 内存2 {n:2}

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}) |

1
2
3
4
5
|    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}

总结

JavaScrip
t中未有引用传递,独有值传递。对象(援用类型)的传递只是拷贝一个新的引用,那一个新的引用能够访谈原来对象上的属性,不过那个新的援用作者是身处别的一个格子上的值,直接往那一个格子赋新的值,并不会影响原来的对象。本文起始所议论的
Node.js
热更新时遭逢的也是其一难题,区别是目的自己改造了,而原本拷贝出来的引用还指向旧的内存,所以通过旧的援用调用不到新的不二等秘书诀。

Node.js 并不曾对 JavaScript 施加黑法力,当中的引用难题还是是 JavaScript
的内容。如 module.exports 与 exports
那样暗藏了一些细节轻巧使人误会,本质照旧 JavaScript
的难点。另外推荐二个有关 Node.js 的进级教程 《Node.js
面试》。

注[1]:

  1. 规矩说,模块在函数内表明有一些谭浩强的感到。
  2. 把 b = include(xxx)
    写在调用内部,还足以经过设置成人中学间件绑定在公私位置来写。
  3. 除此之外写在调用内部,也能够导出三个工厂函数,每趟使用时 b().num
    一下调用也足以。
  4. 还能透过中间件的款型绑定在框架的公用对象上(如:ctx.b =
    include(xxx))。
  5. 要落实如此的热更新必需在架设上将要严谨幸免旧代码被援引的只怕性,不然很轻松写出内部存款和储蓄器泄漏的代码。

    1 赞 收藏
    评论

图片 1

发表评论

电子邮件地址不会被公开。 必填项已用*标注