当在node.js中要求同一模块时,require()函数如何工作

7

当在node.js中多次需要一个模块时,它会返回相同的对象,因为require()会缓存之前的调用。

假设我有一个主记录器模块,可以注册子记录器模块。(这些通过主记录器模块的log()函数实际进行日志记录。但这与此处无关。)

在主记录器模块中,我有类似以下内容以添加子模块:

module.addRedisLogger = function(rclient) {
     modulesArray.push(require('./redis.js')(rclient, loggingEnabled, module));
}

当我创建一个redis客户端实例时,我可以立即像这样为其添加日志记录器:
var sub = redis.createClient();
logger.addRedisLogger(sub);

在子模块中,我开始像这样记录日志:

module.startLogging = function() {
    rclient.on("message", messageCallback);
    rclient.on("pmessage", pmessageCallback);
}

请停止这样记录日志:

module.stopLogging = function() {
    rclient.removeListener("message", messageCallback);
    rclient.removeListener("pmessage", pmessageCallback);

}

但据我所知,使用这种技术只能分配一个redis日志记录器实例,因为分配第二个将返回与第一个相同的require()对象,因此传递新的redis参数将覆盖先前的值。因此,在第一个redis日志记录器实例中停止记录是不可能的,因为调用它会停止第二个实例中的记录。
让我们看一个例子:
var sub1 = redis.createClient();
var sub2 = redis.createClient();

sub1.subscribe("joinScreen1");
sub2.subscribe("joinScreen2");

logger.addRedisLogger(sub1);
logger.addRedisLogger(sub2);

// running redis-cli PUBLISH joinScreen1 message1
// running redis-cli PUBLISH joinScreen2 message2

logger.log("Lets stop the first logger);
logger.modulesArray[0].stopLogging()

// running redis-cli PUBLISH joinScreen1 message1
// running redis-cli PUBLISH joinScreen2 message2

我期望你可以提供以下输出结果:
// Message received on channel joinScreen1: message1
// Message received on channel joinScreen2: message2
// Message received on channel joinScreen1: message1

我们应该得到这个,因为第一个记录器现在指向第二个实例。所以redis也指向了第二个客户端。
但是我得到的是:
// Message received on channel joinScreen1: message1
// Message received on channel joinScreen2: message2
// Message received on channel joinScreen2: message2

所以它按照程序设计的预期工作,但不按照代码的预期工作。所以我想让它像现在这样工作,但我不明白为什么它会这样工作。

更新:

详细说明

module.js

var util = require("util");

module.exports = function () {
var module = {};

    module.show = function() {
        console.log(util.client);
    }

    module.set = function(value) {
        util.client= value;
    }

    return module;
};

main.js

var util = require("util");
util.client = "waaaa";

 var obj = require('./module')();

 obj.show();
 obj.set("weeee");

console.log(util.client);

运行 main.js 将会产生以下输出:
C:\Users\me\Desktop>node main.js
waaaa
weeee

因此,require() 返回的对象与第一次相同。如果自那时起我进行了更改,则更改也会存在,因为它是相同的对象。

现在假设变量 redis 与这里的 client 相同,并保存对 redis 连接的引用。当构造函数第二次运行时,它将覆盖第一个构造函数,这就是为什么我希望从第一个 redis 客户端的记录器中获得通知,因为没有指向它的引用,因此无法删除监听器。


我很难从帖子内容中解读出一个问题。标题中的问题可能是重复的,但我认为这不是你要问的,你似乎已经知道了require()的工作原理和缓存。最终这将成为一个逻辑问题,而且这个逻辑(代码)在帖子中组织得不够清晰,我们无法轻松地跟进它(太多东西都缺失了)。 - Kevin B
我更新了我的问题,希望现在更容易理解了。 - NoNameProvided
2个回答

4
像你所说的,require会缓存返回的对象。在您的用例中,require只是返回一个function对象,仅此而已。这个函数被调用时,会改变内存中的某个引用,因此它可以按预期工作。缓存函数并不等同于缓存该函数调用的输出。此外,在Node中返回构造函数是公认的暴露类的方式,因为每次需要新对象时,只需调用该函数即可。换句话说,您所做的完全正确。
例如:
案例A
module.exports = new Date();

案例B

module.exports = function() {
  return new Date();
}

在情况A中,require函数将缓存日期对象,并且始终返回对同一对象的引用。在情况B中,require函数将缓存函数对象,并且始终返回对同一函数的引用。区别在于,在B中调用该缓存的函数时,每次函数都会返回一个新的日期对象。


我更新了我的问题,我需要对你的答案进行一些澄清。如果我将其导出为函数,它将每次运行,但仍会将samb对象返回给我,这样做:var module = request("module"); module.x = "y"; console.log(request("module").x);将在控制台上打印“y”,对吗?我想要得到一个新的对象,它没有定义x参数。 - NoNameProvided
没错,它应该打印“y”。你为什么认为它应该等于其他东西呢?变量module是指向request("module")的指针,因此当您更改属性x时,它将不可避免地将该更改传播到request("module"),因为两者都是指向完全相同的对象的指针。 - Yuri Zarubin
是的,但我在我的真实示例中也是这样做的,但我得到了不同的结果。请参见我问题的更新部分(长版本)。 - NoNameProvided
不,你得到的结果是完全一致的。在两个模块中,你都调用了 require('util'),这意味着在两个模块中你将得到完全相同的对象。在 main.js 中,你将 require('util').client 设置为 "waa",然后调用 module.js 构造函数,并使用 set 方法将 require('util').client 设置为 "wee"。你正在改变完全相同的对象,因为 require('util')main.jsmodule.js 中返回给你相同的引用。我没有看到任何不一致的地方。 - Yuri Zarubin
谢谢您的回复!我是在谈论我问题中包含的真实代码示例,不是这里的代码,请查看更新2以了解更多细节。 - NoNameProvided
@未提供姓名 我无法回答你的问题,因为我不理解问题在哪里。请用清晰明了的语言描述在您调用 logger.modulesArray [0] .stopLogging() 后会发生什么,以及您认为应该发生什么。您显示的输出很令人困惑,很难理解正在发生什么。我会假设它应该停止记录消息#1,并继续记录消息#2,对吗?这不是正在发生的事情吗?如果不是,那么正在发生什么呢? - Yuri Zarubin

2
当你说这个时: > 但是据我所知,使用这种技术只能分配一个redis日志记录器实例,因为分配第二个将返回与第一个相同的require()对象,因此传递新的redis参数会覆盖先前的值。
在你的情况下并不是这样。你的模块正在导出一个单一的函数:
module.exports = function (rclient, ploggingEnabled, logger) {

每次调用`require('./redis.js')`都会返回该函数。如果您修改了该函数的属性,然后再次调用`require()`,您将确实看到您所做的修改。
(require('./redis.js')).x = "waaa";
console.log((require('./redis.js')).x); // "waaa"

但是,当你实际调用函数而不是修改它的属性时,会发生不同的事情:通过声明 startLogging()stopLogging() 函数创建了一个闭包,它们分别操作 作用域链 中更高层的变量。这意味着每次调用此导出函数都会创建一个新的闭包,其中包含指向rclientploggingEnabledlogger的私有指针。如果你保存对此函数返回值的引用(通过将其push()modulesArray中),那么你应该能够在以后的某个时候访问每个闭包并执行所需的任何清理操作。
modulesArray.forEach(function(el, idx, arr) {
  el.stopLogging();
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接