Search code examples
node.jseventemitter

Derived node.js EventEmitter will call the wrong listener when in a named object property


I've been working on creating my own home automation hub using a handful of node.js packages (Express, mqtt, socket.io) with a MongoDB database and Angular running in the client. This project is the first time I've worked with any JavaScript, so it's safe to say I'm a bit of a noob.

I've been troubleshooting an issue with derived EventEmitters calling the wrong listener function when referenced as a named property. Here's a minimal example that will demonstrate the problem:

var EventEmitter = require('events').EventEmitter;
var debug = require('debug')('test.js');
var util = require('util');

var TestEmitter = function (initialState, name) {
    var self = this;
    EventEmitter.call(self);
    this.state = initialState;
    this.name = name;
    this.setState = function (newState) {
        self.emit('change', newState, this.state);
        self.state = newState;
    };
};
util.inherits(TestEmitter, EventEmitter);

var myObj = {
    ary: [new TestEmitter(false, 'ary1'), new TestEmitter(true, 'ary2')],
    named: {
        name1: new TestEmitter(3, 'name1'),
        name2: new TestEmitter(4, 'name2')
    }
};

myObj.ary.forEach(function (aryEmitter) {
    aryEmitter.on('change', function (newState) {
        debug(aryEmitter.name + ' changed to: ' + newState);
    });
});
for (var prop in myObj.named) {
    var currEmitter = myObj.named[prop];
    debug('prop = ' + prop);
    debug('name = ' + currEmitter.name);
    currEmitter.on('change', function (newState) {
        debug(currEmitter.name + ' changed to: ' + newState);
    });
}

myObj.ary[0].setState(true);
myObj.ary[1].setState(false);
myObj.named.name1.setState(4);
myObj.named.name2.setState(5);

Essentially, TestEmitter is an object derived from EventEmitter that will broadcast a change event when its setState method is called.

myObj is a reference to four TestEmitter instances -- two in an array, and two in named properties. After creating myObj, I register a listener for each of their change events that simply writes debugging output to the console with the name of the TestEmitter instance whose callback is being invoked.

However, the named TestEmitter references don't work the way that I would expect. The call to both myObj.named.name1.setState(4) as well as myObj.named.name2.setState(5) will both execute the callback function that I registered for the EventEmitter myObj.named.name2. Running everything above will produce the following output:

test.js prop = name1 +0ms
test.js name = name1 +5ms
test.js prop = name2 +0ms
test.js name = name2 +0ms
test.js ary1 changed to: true +0ms
test.js ary2 changed to: false +0ms
test.js name2 changed to: 4 +0ms
test.js name2 changed to: 5 +0ms

Can anyone offer any help? I've read a fair amount about the best way to create derived EventEmitters and it looks like I'm taking the right approach, so I'm a bit stumped.

Thanks for reading and any help you're able to offer!


Solution

  • Your problem becomes at this part of the code:

    for (var prop in myObj.named) {
        var currEmitter = myObj.named[prop];
        debug('prop = ' + prop);
        debug('name = ' + currEmitter.name);
        currEmitter.on('change', function (newState) {
            debug(currEmitter.name + ' changed to: ' + newState);
        });
    }
    

    So that when an event is fired, inside the function handler, currEmitter has the value of the last element of loop.

    One simple fix is to use encapsule it in a function, to create a new scope, and do the things inside this function :

    for (var prop in myObj.named) {
        var currEmitter = myObj.named[prop];
        (function(currEmitter){
          currEmitter.on('change', newState=>{
            debug(currEmitter.name + ' changed to: ' + newState);
          });
        })(currEmitter);
    }
    

    Or you can use Object.keys():

    Object.keys(myObj.named).forEach(function(key){
      var currEmitter = myObj.named[key];
      currEmitter.on('change', function(newState){
        debug(currEmitter.name + ' changed to: ' + newState);
      });
    });