阅读更多
引用
作者简介:黄鼎恒,饿了么Node Team负责人,Node/C程序员,饿了么前端实时监控系统主要开发者。
责编: 陈秋歌,欢迎技术投稿、给文章纠错,请发邮件至chenqg@csdn.net,或加微信:Rachel_qg。
声明: 本文为《程序员》原创文章,未经允许请勿转载,更多精彩文章请订阅 2017 年《程序员》

导读:对于从PHP转到Node.js的作者而言,Node.js编辑完代码后必须重启真是件麻烦事。在不重启情况下热更新Node.js代码,是本文重要讨论的话题。而解决该问题,JavaScript的引用成为了关键。层层剖析,抽丝剥茧,带你了解问题本质及解决之道。

早期学习Node.js的时候,有挺多是从PHP转过来的,当时有部分人对于Node.js编辑完代码需要重启一下表示麻烦(PHP不需要这个过程),于是社区里的朋友就开始提倡使用node-supervisor这个模块来启动项目,可以编辑完代码之后自动重启。不过相对于PHP而言依旧不够方便,因为Node.js在重启以后之前的上下文都丢失了。

虽然可以通过将session数据保存在数据库或者缓存中来减少重启过程中的数据丢失,不过如果是在生产的情况下,更新代码的重启间隙是没法处理请求的(PHP可以,另外那个时候还没有cluster)。由于这方面的问题,加上本人是从PHP转到Node.js的,于是从那时开始思考有没有办法可以在不重启的情况下热更新Node.js的代码。

最开始把目光瞄向了require这个模块。想法很简单,因为Node.js中引入一个模块都是通过require这个方法来加载的。于是就开始思考require能不能在更新代码之后再次require一下。尝试如下:
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);

exports.num = 1024;

两个JS文件写好之后,从a.js启动,刷新页面会输出b.js中的1024,然后修改b.js文件中导出的值,例如修改为2048。再次刷新页面依旧是原本的1024。

再次执行一次require并没有刷新代码。require在执行的过程中加载完代码之后会把模块导出的数据放在require.cache中。require.cache是一个{}对象,以模块的绝对路径为key,该模块的详细数据为value。于是便开始做如下尝试:
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内部可以自动刷新,让外面拿到最新的代码。

但是实际的开发过程中,这样很快就碰到了问题。我们希望的代码可能是这样:
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。也就是说这样写才可以:
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怎么实现,本文就不深入讨论了,因为这个技巧使用度不高,写起起来不是很优雅①,反而这之间有一个更重要的问题————JavaScript的引用。

JavaScript的引用与传统引用的区别
要讨论这个问题,我们首先要了解JavaScript的引用于其他语言中的一个区别,在C++中引用是直接可以修改外部的值:
#include <iostream>

using namespace std;

void test(int &p) // 引用传递
{
    p = 2048;
}

int main()
{
    int a = 1024;
    int &p = a; // 设置引用p指向a

    test(p); // 调用函数

    cout << "p: " << p << endl; // 2048
    cout << "a: " << a << endl; // 2048
    return 0;
}


而在JavaScript中:
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中均是值传递,对象在传递的过程中是拷贝了一份新的引用。

为了理解这个比较拗口的结论,让我们来看一段代码:
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。

打个比方最初设置var data = obj.data的时候,内存中的情况大概是:

所以通过data.xx可以修改到obj.data的内存1。

然后设置data = xxx,由于data是拷贝的一个新的值,只是这个值是一个引用(指向内存1)罢了。让它等于另外一个对象就好比:

让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_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

Node.js中的exports就是拷贝的一份module.exports的引用。通过exports可以修改Node.js当前文件导出的属性,但是不能修改到当前模块本身。通过module.exports才可以修改到其本身。表现上来说:
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);

按照开始的结论我们可以一步步的来看这个问题:
var a = {n: 1};    // 引用a指向内存1{n:1}
var b = a;  // 引用b => a => { n:1 }


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

a 虽然是引用,但是JavaScript是值传的这个引用,所以被修改不影响原本的地方。

所以最后的结果
  • 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的问题。另外推荐一个关于 Node.js 的进阶教程 《Node.js 面试》。

注①:
  • 老实说,模块在函数内声明有点谭浩强的感觉。
  • 把b = include(xxx)写在调用内部,还可以通过设置成中间件绑定在公共地方来写。
  • 除了写在调用内部,也可以导出一个工厂函数,每次使用时b().num一下调用也可以。
  • [*]还可以通过中间件的形式绑定在框架的公用对象上(如:ctx.b = include(xxx))。
  • 大小: 2.8 KB
  • 大小: 2.8 KB
  • 大小: 2.6 KB
  • 大小: 4.7 KB
0
0
评论 共 0 条 请登录后发表评论

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 在Node.js中看JavaScript的引用

    作者简介:黄鼎恒,饿了么Node Team负责人,Node/C程序员,饿了么前端实时监控系统主要开发者。 责编: 陈秋歌,欢迎技术投稿、给文章纠错,请发邮件至chenqg#csdn.net,或加微信:Rachel_qg。 声明: 本文为...

  • javascript实现图片轮播_Node.js实现将文字与图片合成技巧

    我的常规做法就是网上搜一张图片,然后利用 PhotoShop,在图片上加入文章标题,然后导出生成图片,如下图所示:上图实际就是在一张背景图中,加入了一行文字。操作步骤简单,但是每次都需要打开PhotoShop修改文字,...

  • 深入理解 Node.js 的 Inspector

    Node.js 提供的 Inspector非常强大,不仅可以用来调试 Node.js 代码,还可以实时收集 Node.js 进程的 Heap Snapshot、Cpu Profile 等...

  • Node.js 网页瘸腿爬虫初体验

    延续上一篇,想把自己博客的文档标题利用Node.js的request全提取出来,于是有了下面的初哥爬虫,水平有限,这只爬虫目前还有点瘸腿,请看官你指正了。 // 内置http模块,提供了http服务器和客户端功能 var ...

  • 基于node.js的web程序入门

    看过很多node.js的入门教学,哎,小的就是介绍hello world的页面输出,太简单,不够实用;大的就是给一个web工程的打包文件,初学者很难掌握。都不好。这个教程是写给node.js初学者,甚至是js的初学者的。

  • Node.js 网页爬虫再进阶,cheerio助力

    读取app2.js // 内置http模块,提供了http服务器和客户端功能 var http=require(&quot;http&quot;); // cheerio模块,提供了类似jQuery的功能 var cheerio = require(&quot;cheerio&quot;); // 内置文件...

  • 【黑马程序员】react学习笔记

    DOM 浏览器中的概念 用JS对象表示页面中的元素并提供操作对象的api 虚拟DOM 框架中的概念 用JS对象模拟页面上的DOM和DOM嵌套,主要目的是实现页面上元素的高效更新 Diff算法 :tree diff, component diff, ...

  • Node.js 网页瘸腿稍强点爬虫再体验

    Node.js 本地Xhr取得Node.js服务端数据的例子 Node.js node主文件找不到时报出的Error:Cannot find module异常 给java类加static修饰编译器会说什么? AngularJS中Route例子 AngularJS中自定义过滤器 AngularJS中...

  • package-lock.json的作用

    在运行项目的时候常会遇到莫名其妙的报错问题,这个时候逼不得已会执行删除package-lock.json和node_modules然后npm cache clean --force清理缓存,npm i 重新安装依赖的操作,package-lock.json会根据package.json...

  • Javascript:一个屌丝的逆袭

    是的, 我就是鼎鼎大名的Javascript, 典型的高富帅,前端编程之王,数以百万计的程序员使用我来编程。 如果你没有用过我就太out了。  不过当我是一个屌丝时,真的没有想到能发展到如今的地位...... 第一章:出世...

  • Javascript:一个屌丝的逆袭之路

    是的, 我就是鼎鼎大名的Javascript, 典型的高富帅,前端编程之王,数以百万计的程序员使用我来编程。 如果你没有用过我就太out了。 不过当我是一个屌丝时, 真的没有想到能发展到如今的地位… 第一章:出世 我出生...

  • vue项目的打包与优化

    我使用的是 vue3.0脚手架,在vue3.0中没有配置文件,我首先在项目根目录下创建了vue.config.js文件,在里面进行打包与优化的配置。 使用脚手架进行run build打包,静态资源404问题 在vue.config.js中设置静态资源...

  • 大四学年在某软件公司实习(java + groovy + vue.js)近半年工作总结

    虽然在这里呆的时间不长,平时除了几个熟悉的同事交流的比较多之外,我对其他很多人知之甚少。无知带来的最大的一个问题便是傲慢和偏见,这是每一个人都无法改变的,即使你后天经过再多努力的修养与历练,也很难完全...

  • JavaScript介绍.zip

    javascript,JavaScript 最初由 Netscape 公司的 Brendan Eich 在 1995 年开发,用于 Netscape Navigator 浏览器。随着时间的推移,JavaScript 成为了网页开发中不可或缺的一部分,并且其应用范围已经远远超出了浏览器,成为了全栈开发的重要工具。

  • 上位机开发罗克韦尔abcip通信协议详解

    上位机开发罗克韦尔abcip通信协议详解 1.注册会话命令详解 6500 0400 00000000 00000000 0000000000000000 00000000 0100 0000 响应 6500 0400 05000400 00000000 0000000000000000 00000000 0100 0000 6500:注册请求命令 0400:服务长度(0100 0000) 00000000:会话句柄 (由PLC生成) 00000000:状态默认 0000000000000000:发送方描述,默认0 00000000:选项,默认0 0100:协议版本,默认1 0000:选项标记,默认0

  • Microsoft SPY++ 工具及使用教程

    Spy++ (SPYXX.EXE) 是一个基于 Win32 的实用工具,提供系统进程、线程、窗口和窗口消息的图形视图。 Spy++ 有两个版本。 第一个版本,名为 Spy++ (spyxx.exe),用于显示发送到在 32 位进程中运行的窗口的消息。 例如,在 32 位进程中运行的 Visual Studio。 因此,可以使用 Spy++ 来显示发送到“解决方案资源管理器” 中的消息。 由于 Visual Studio 中大多数生成的默认配置都是在 32 位进程中运行的,因此如果已安装所需组件,则第一个版本的 Spy++ 就是在 Visual Studio 中的“工具”菜单上可用的那一个。 第二个版本,名为 Spy++(64 位)(spyxx_amd64.exe),用于显示发送到在 64 位进程中运行的窗口的消息。 例如,在 64 位操作系统上,记事本在 64 位进程中运行。 因此,可以使用 Spy++(64 位)来显示发送到记事本的消息。 详细的使用说明请见:https://blog.csdn.net/huang1600301017/article/details/138137

  • js导出excel封装【原生、配置式】 示例

    导出excel示例

  • HTML2-iOs-App模板官网落地页APP主页产品宣传页源码 landing静态页面.zip

    HTML2-iOs-App模板官网落地页APP主页产品宣传页源码 landing静态页面

  • 围绕talib-ruby项目的更高级别的包装-Ruby

    计算机技术是指评价计算机系统的各种知识和技能的总称。它涵盖了计算机硬件、软件、网络和信息安全等方面。计算机技术的发展使我们能够进行高效的数据处理、信息存储和传输。现代计算机技术包括操作系统、数据库管理、编程语言、算法设计等。同时,人工智能、云计算和大数据等新兴技术也在不断推动计算机技术的进步。计算机技术的应用广泛,涵盖了各个领域,如商业、医疗、教育和娱乐等。随着计算机技术的不断革新,我们可以更加高效地实现预期自动化、标准化

  • 基于Java聊天室程序(java) .zip

    基于Java聊天室程序(java)

Global site tag (gtag.js) - Google Analytics