NodeJS中的模块是单例的吗?

本文翻译自 Lazlojuly 的 are-node-js-modules-singletons。

本文从属于笔者的NodeJS入门与最佳实践中的NodeJS 基础系列文章,包括NodeJS 入门、NodeJS 模块导出与解析、NodeJS IOStream、NodeJS HTTPS这几部分。

笔者之前在使用require导入模块时,特别是在导入有状态的模块时,笔者会考虑其是否在多次导入情况下依然保持单例特性,或者说对于同一个文件在不同路径下导入时,是否能够识别为一致?本文即是对该特性进行解析。

NodeJS的模块默认情况下是单例性质的,不过其并不能保证如我们编程时设想的那样一定是单例,根据NodeJS的官方文档中描述,某个模块导入是否为单例受以下两个因素的影响:

  • Node 模块的缓存机制是大小写敏感的,譬如如果你require('/foo')与require('/FOO')会返回两个不同的对象,尽管你的foo与FOO是完全相同的文件。
  • 模块是基于其被解析得到的文件名进行缓存的,鉴于不同的模块会依赖于其被调用的路径进行缓存鉴别,因此并不能保证你使用require('foo')会永远返回相同的对象,可能会根据不同的文件路径得到不同的对象。

创建新的NodeJS模块

根据NodeJS文档所述,文件和模块是一一对应的关系。这个也是解释上文提及的模块缓存机制的基础,我们首先创建一个简单的模块:


  1. // counter.js  
  2.  
  3. let value = 0 
  4.  
  5. module.exports = { 
  6.   increment: () => value++, 
  7.   get: () => value, 

在counter.js中我们创建了某个私有变量,并且只能通过公共的increment与get方法进行操作。在应用中我们可以如下方法使用该模块:


  1. // app.js 
  2. const counter = require(‘./counter.js’) 
  3.  
  4. counter.increment() 
  5. counter.increment() 
  6.  
  7. console.log(counter.get()) // prints 2 
  8. console.log(counter.value) // prints undefined as value is private  

Module Caching

NodeJS会在第一次导入某个模块之后将该模块进行缓存,在官方文档中有如下描述:

Every call to require(‘foo’) will get exactly the same object returned, if it would resolve to the same file.

我们也可以通过如下简单的例子来验证这句话:


  1. // app-singleton.js 
  2.  
  3. const counter1 = require(‘./counter.js’) 
  4. const counter2 = require(‘./counter.js’) 
  5.  
  6. counter1.increment() 
  7. counter1.increment() 
  8. counter2.increment() 
  9.  
  10. console.log(counter1.get()) // prints 3 
  11. console.log(counter2.get()) // also prints 3  

可以看出尽管我们两次导入了该模块,但是还是指向了同一个对象。不过并不是每次我们导入同一个模块时,都会得到相同的对象。在NodeJS中,模块对象有个内置的方法:Module._resolveFilename(),其负责寻找require中合适的模块,在找到正确的文件之后,会根据其文件名作为缓存的键名。官方的搜索算法伪代码为:


  1. require(X) from module at path Y 
  2. 1. If X is a core module, 
  3.    a. return the core module 
  4.    b. STOP 
  5. 2. If X begins with './' or '/' or '../' 
  6. a. LOAD_AS_FILE(Y + X) 
  7.       1. If X is a file, load X as JavaScript text.  STOP 
  8.       2. If X.js is a file, load X.js as JavaScript text.  STOP 
  9.       3... 
  10.       4... 
  11. b. LOAD_AS_DIRECTORY(Y + X) 
  12.       1. If X/package.json is a file, 
  13.          a. Parse X/package.json, and look for "main" field. 
  14.          b. let M = X + (json main field) 
  15.          c. LOAD_AS_FILE(M) 
  16.       2. If X/index.js is a file, load X/index.js as JS text.  STOP 
  17.       3... 
  18.       4... 
  19. 3. LOAD_NODE_MODULES(X, dirname(Y)) 
  20. 4. THROW "not found"  

简单来说,加载的逻辑或者说优先级为:

  • 优先判断是不是核心模块
  • 如果不是核心模块则搜索node_modules
  • 否则在相对路径中进行搜索

解析之后的文件名可以根据module对象或得到:


  1. // counter-debug.js 
  2.  
  3. console.log(module.filename) // prints absolute path to counter.js 
  4. console.log(__filename) // prints same as above 
  5. // i get: "/Users/laz/repos/medium/modules/counter-debug.js" 
  6.  
  7. let value = 0 
  8.  
  9. module.exports = { 
  10.   increment: () => value++, 
  11.   get: () => value,  

在上述的例子中我们可以看出,解析得到的文件名即使被加载模块的绝对路径。而根据文件与模块一一映射的原则,我们可以得出下面两个会破坏模块导入单例性的特例。

Case Sensitivity

在大小写敏感的文件系统中或者操作系统中,不同的解析之后的文件可能会指向相同的文件,但是其缓存键名会不一致,即不同的导入会生成不同的对象。


  1. // app-no-singleton-1.js 
  2. const counter1 = require('./counter.js') 
  3. const counter2 = require('./COUNTER.js') 
  4.  
  5. counter1.increment() 
  6. console.log(counter1.get()) // prints 1 
  7. console.log(counter2.get()) // prints 0, not same object as counter1 
  8.  
  9. /*  
  10. We have two different resolved filenames: 
  11. - “Users/laz/repos/medium/modules/counter.js” 
  12. - “Users/laz/repos/medium/modules/COUNTER.js” 
  13. */  

在上面的例子中,我们分别用counter、COUNTER这仅仅是大小写不同的方式导入相同的某个文件,如果是在某个大小写敏感的系统中,譬如UBUNTU中会直接抛出异常:

解析为不同的文件名

当我们使用require(x)并且x不属于核心模块时,其会自动搜索node_modules文件夹。而在npm3之前,项目会以嵌套的方式安装依赖。因此当我们的项目依赖module-a与module-b,并且module-a与module-b也相互依赖时,其会生成如下文件路径格式:


  1. // npm2 installed dependencies in nested way 
  2. app.js 
  3. package.json 
  4. node_modules/ 
  5. |---module-a/index.js 
  6. |---module-b/index.js 
  7.     |---node_modules 
  8.         |---module-a/index.js  

这样的话,我们对于同一个模块就有两个副本,那当我们在应用中导入module-a时,岂会载入如下文件:


  1.  // app.js 
  2. const moduleA = require(‘module-a’) 
  3. loads: “/node_modules/module-a/index.js”  

而从module-b中载入module-a时,其载入的是如下文件:


  1.  // /node_modules/module-b/index.js 
  2. const moduleA = require(‘module-a’) 
  3. loads “/node_modules/module-b/node_modules/module-a/index.js”  

不过在npm3之后,其以扁平化方式进行文件加载,其文件目录结构如下所示:


  1.  // npm3 flattens secondary dependencies by installing in same folder 
  2. app.js 
  3. package.json 
  4. node_modules/ 
  5. |---module-a/index.js 
  6. |---module-b/index.js  

不过此时就存在另一个场景,即我们应用本身依赖module-a@v1.1与module-b,而module-b又依赖于module-a@v1.2,在这种情况下还是会采用类似于npm3之前的嵌套式目录结构。这样的话对于module-a一样会产生不同的对象,不过此时本身就是不同的文件了,因此相互之间不会产生冲突。

作者:王下邀月熊_Chevalier

来源:51CTO

时间: 2024-11-03 08:40:49

NodeJS中的模块是单例的吗?的相关文章

java-项目中什么时候用单例什么时候用多例?

问题描述 项目中什么时候用单例什么时候用多例? 在网上看朋友说,action一定要用多例,保证数据安全.那么在项目开发中,通常哪些地方用单例哪些地方用多例呢? 解决方案 单例多例需要搞明白两个问题: 1. 什么是单例多例: 2. 如何产生单例多例: 3. 为什么要用单例多例 4. 什么时候用单例,什么时候用多例: 1. 什么是单例多例: 所谓单例就是所有的请求都用一个对象来处理,比如我们常用的service和dao层的对象通常都是单例的,而多例则指每个请求用一个新的对象来处理,比如action;

ios-iOS能讲解下在继承uiview中该怎样用单例并传值

问题描述 iOS能讲解下在继承uiview中该怎样用单例并传值 最好有简单代码实例 真的十分感谢 就是自己写了单独的错误提示框 想实例化一次后 就不需要实例了 解决方案 错误提示框一般都是使用的时候显示,不使用的时候释放,这样比较合理,不要滥用单例,如果觉得每次实例化麻烦那就写个类方法,方法里面根据传入的字符串创建实例然后返回实例,你只需要在要用的类中去接收这个实例,然后控制显示和隐藏就好了 解决方案二: 那你可以单独写一个类用于处理,再分别引入就可以了.如果是单例你的警告内容更改也会比较麻烦

ios中的线程安全单例实现

我们这里重点讨论的是线程安全,而不是单例的严格实现(若需要严格实现还需要复写一些方法,更改一些实现代码): gcd实现:   static AccountManager *sharedAccountManagerInstance = nil;  + (AccountManager *)sharedManager {      static dispatch_once_t predicate; dispatch_once(&predicate, ^{                  shared

NodeJS中Buffer模块详解_node.js

一,开篇分析 所谓缓冲区Buffer,就是 "临时存贮区" 的意思,是暂时存放输入输出数据的一段内存. JS语言自身只有字符串数据类型,没有二进制数据类型,因此NodeJS提供了一个与String对等的全局构造函数Buffer来提供对二进制数据的操作.除了可以读取文件得到Buffer的实例外,还能够直接构造,例如: 复制代码 代码如下: var buffer = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]) ; Buffer与字符串类似,除了

Singleton(单例)模式和Double-Checked Locking(双重检查锁定)模式

问题描述 现在,不管开发一个多大的系统(至少我现在的部门是这样的),都会带一个日志功能:在实际开发过程中,会专门有一个日志模块,负责写日志,由于在系统的任何地方,我们都有可能要调用日志模块中的函数,进行写日志.那么,如何构造一个日志模块的实例呢?难道,每次new一个日志模块实例,写完日志,再delete,不要告诉我你是这么干的.在C++中,可以构造一个日志模块的全局变量,那么在任何地方就都可以用了,是的,不错.但是,我所在的开发部门的C++编码规范是参照Google的编码规范的. 全局变量在项目

浅谈NodeJS中require路径问题

  学习Nodejs也是出于对这个新产物的好奇,而且有两个重要项目也需要他的支持,所以要好好学学这个新语种.在nodejs中,模块大概可以分为核心模块和文件模块.核心模块是被编译成二进制代码,引用的时候只需require表示符即可 项目需要用nodejs,感觉nodejs是前端装逼神器了,是通向全栈工程师的必经之路哇,接下来开始踏上学习nodejs的征程.下面是第一个hello,world的程序. 1.server.js文件,这相当于服务器脚本. ? 1 2 3 4 5 6 7 8 9 10 1

nodejs的require模块(文件模块/核心模块)及路径介绍_基础知识

在nodejs中,模块大概可以分为核心模块和文件模块. 核心模块是被编译成二进制代码,引用的时候只需require表示符即可,如(require('net')). 文件模块,则是指js文件.json文件或者是.node文件.在引用文件模块的时候后要加上文件的路径:/.../.../xxx.js表示绝对路径../xxx.js表示相对路径(同一文件夹下的xxx.js),../表示上一级目录.如果既不加/.../.../又不加./的话,则该模块要么是核心模块,要么是从一个node_modules文件夹

Nodejs中作用域问题的详细解析

在JS中有全局作用域和函数作用域,而在Nodejs中也自己的作用域,分为全局作用域(global)和模块作用域. js作用域: 以前学js的时候我们的全局对象是window,如: vara = 10; console.log(window.a); 我们定义的全局变量默认是给window添加一个属性或者方法. functionfn(){   varnum = 22; } console.log(num); 报错,因为num是在函数中定义的,在函数外部是访问不了函数内部的变量的,虽然可以通过闭包来访

(单例设计模式中)懒汉式与饿汉式在多线程中的不同

/*  目的:分析一下单例设计模式中,懒汉式与饿汉式在多线程中的不同!  开发时我们一般选择饿汉式,因为它简单明了,多线程中不会出现安全问题!  而饿汉式需要我们自己处理程序中存在的安全隐患,但是饿汉式的程序技术含量更高! */ /* class SinglePerson implements Runnable{    private static SinglePerson ss = new SinglePerson("hjz", 22);//恶汉式    private int ag