阅读更多
引用
作者简介:黄鼎恒,饿了么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)近半年工作总结

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

  • 基于SSM+JSP+HTML的东风锻造有限公司重大停管理系统(Java毕业设计,附源码,数据库,教程).zip

    Java 项目, Java 毕业设计,Java 课程设计,基于 ssm 开发的,含有代码注释,新手也可看懂。毕业设计、期末大作业、课程设计、高分必看,下载下来,简单部署,就可以使用。 包含:项目源码、数据库脚本、软件工具等,前后端代码都在里面。 该系统功能完善、界面美观、操作简单、功能齐全、管理便捷,具有很高的实际应用价值。 项目都经过严格调试,确保可以运行! 1. 技术组成 前端:jsp 后台框架:SSM 开发环境:idea 数据库:MySql(建议用 5.7 版本,8.0 有时候会有坑) 数据库工具:navicat 部署环境:Tomcat(建议用 7.x 或者 8.x 版本), maven 2. 部署 如果部署有疑问的话,可以找我咨询 Java工具包下载地址: https://pan.quark.cn/s/eb24351ebac4

  • 数据库系统课程设计报告-体育项目比赛管理系统设计与开发

    一、系统需求分析 1 (一)需求概述 1 (二)业务流分析 1 从运动员角度分析 1 (三)数据流分析 4 (四)数据字典 5 二、数据库概念结构设计 6 (一)实体分析 6 (二)属性分析 6 (三)联系分析 8 (四)概念模型分析(.PDM图) 9 三、数据库逻辑结构设计 9 (一)概念模型转化为逻辑模型 9 1.一对一关系的转化 9 2.一对多关系的转化 9 3.多对多关系的转化 10 (二)逻辑模型设计(.PDM图) 10 四、 数据库物理实现(一)表设计 10 (一)表设计 10 (二)创建表和完整性约束代码设计 11 五、数据库功能调试 15 (一)运动员管理模块 15 (二)负责人管理模块 16 (三)系统管理员管理模块 17 六、设计系统前台软件 21 (一)开发软件选择 21 (二)软件功能要求与设计 22 (三)软件功能实现 22 (四)系统测试 24 七、设计总结 27

  • 基于SSM+JSP的文物管理系统+数据库(Java毕业设计,包括源码,教程).zip

    Java 项目, Java 毕业设计,Java 课程设计,基于 SpringBoot 开发的,含有代码注释,新手也可看懂。毕业设计、期末大作业、课程设计、高分必看,下载下来,简单部署,就可以使用。 包含:项目源码、数据库脚本、软件工具等,前后端代码都在里面。 该系统功能完善、界面美观、操作简单、功能齐全、管理便捷,具有很高的实际应用价值。 项目都经过严格调试,确保可以运行! 1. 技术组成 前端:jsp 后台框架:SSM 开发环境:idea 数据库:MySql(建议用 5.7 版本,8.0 有时候会有坑) 数据库工具:navicat 部署环境:Tomcat(建议用 7.x 或者 8.x 版本), maven 2. 部署 如果部署有疑问的话,可以找我咨询 Java工具包下载地址: https://pan.quark.cn/s/eb24351ebac4

  • 智慧园区整体解决方案-37PPT(46页).pptx

    智慧园区,作为现代化城市发展的新兴模式,正逐步改变着传统园区的运营与管理方式。它并非简单的信息化升级,而是跨越了行业壁垒,实现了数据共享与业务协同的复杂运行系统。在智慧园区的构建中,人们常常陷入一些误区,如认为智慧园区可以速成、与本部门无关或等同于传统信息化。然而,智慧园区的建设需要长期规划与多方参与,它不仅关乎技术层面的革新,更涉及到管理理念的转变。通过打破信息孤岛,智慧园区实现了各系统间的无缝对接,为园区的科学决策提供了有力支持。 智慧园区的核心价值在于其提供的全方位服务与管理能力。从基础设施的智能化改造,如全面光纤接入、4G/5G网络覆盖、Wi-Fi网络及物联网技术的运用,到园区综合管理平台的建设,智慧园区打造了一个高效、便捷、安全的运营环境。在这个平台上,园区管理方可以实时掌握运营动态,包括道路状况、游客数量、设施状态及自然环境等信息,从而实现事件的提前预警与自动调配。同时,智慧园区还为园区企业提供了丰富的服务,如项目申报、资质认定、入园车辆管理及统计分析等,极大地提升了企业的运营效率。此外,智慧园区还注重用户体验,通过信息发布系统、服务门户系统及各类智慧应用,如掌上营销、智慧停车、智能安防等,为园区员工、企业及访客提供了便捷、舒适的生活与工作体验。值得一提的是,智慧园区还充分利用大数据、云计算等先进技术,对园区的能耗数据进行采集、分析与管理,实现了绿色、节能的运营目标。 在智慧园区的建设过程中,还涌现出了许多创新的应用场景。例如,在环境监测方面,智慧园区通过集成各类传感器与监控系统,实现了对园区水质、空气质量的实时监测与预警;在交通管理方面,智慧园区利用物联网技术,对园区观光车、救援车辆等进行实时定位与调度,提高了交通效率与安全性;在公共服务方面,智慧园区通过构建统一的公共服务平台,为园区居民提供了包括平安社区、便民社区、智能家居在内的多元化服务。这些创新应用不仅提升了园区的智能化水平,还为园区的可持续发展奠定了坚实基础。同时,智慧园区的建设也促进了产业链的聚合与发展,通过搭建聚合产业链平台,实现了园区内企业间的资源共享与合作共赢。总的来说,智慧园区的建设不仅提升了园区的综合竞争力,还为城市的智慧化发展树立了典范。它以用户需求为导向,以技术创新为驱动,不断推动着园区向更加智慧、高效、绿色的方向发展。对于写方案的读者而言,智慧园区的成功案例与创新应用无疑提供了宝贵的借鉴与启示,值得深入探索与学习。

  • Java毕业设计-SpringBoot+Vue的基于SpringBoot的冬奥会科普平台(附源码、数据库、教程).zip

    Java 项目, Java 毕业设计,Java 课程设计,基于 SpringBoot 开发的,含有代码注释,新手也可看懂。毕业设计、期末大作业、课程设计、高分必看,下载下来,简单部署,就可以使用。 包含:项目源码、数据库脚本、软件工具等,前后端代码都在里面。 该系统功能完善、界面美观、操作简单、功能齐全、管理便捷,具有很高的实际应用价值。 项目都经过严格调试,确保可以运行! 1. 技术组成 前端:html、javascript、Vue 后台框架:SpringBoot 开发环境:idea 数据库:MySql(建议用 5.7 版本,8.0 有时候会有坑) 数据库工具:navicat 部署环境:Tomcat(建议用 7.x 或者 8.x 版本), maven 2. 部署 如果部署有疑问的话,可以找我咨询 Java工具包下载地址: https://pan.quark.cn/s/eb24351ebac4 后台路径地址:localhost:8080/项目名称/admin/dist/index.html 前台路径地址:localhost:8080/项目名称/front/index.html (无前台不需要输入)

  • MATLAB设计的芯片字符识别(GUI界面设计).zip

    MATLAB设计的芯片字符识别(GUI界面设计)

  • 【工程项目】MATLAB口罩识别[自动定位颜色,多人检测,未戴预警 ].zip

    【工程项目】MATLAB口罩识别[自动定位颜色,多人检测,未戴预警 ]

Global site tag (gtag.js) - Google Analytics