ponyfoo.com

A Less Convoluted Event Emitter Implementation

Fix
A relevant ad will be displayed here soon. These ads help pay for my hosting.
Please consider disabling your ad blocker on Pony Foo. These ads help pay for my hosting.
You can support Pony Foo directly through Patreon or via PayPal.

I believe that the event emitter implementation in Node could be made way better by providing a way to access the functionality directly without using prototypes. This would allow to simply extend any object, such as {}, or { pony: 'foo' }, with event emitting capabilities. Prototypes enforce limitations for little gain, and that’s what we avoid by going around it and merely adding methods on existing objects, without any prototypal inheritance going on.

In this article I’ll explore the implementation that made its way into contra, an asynchronous flow control library I designed.

Event emitters usually support multiple types of events, rather than a single one. Let’s implement, step by step, our own function to create event emitters, or improve existing objects as event emitters. In a first step, I’ll either return the object unchanged, or create a new object if one wasn’t provided.

function emitter (thing) {
  if (!thing) {
    thing = {};
  }
  return thing;
}

Being able to use multiple event types is powerful and only costs us an object to store the mapping of event types to event listeners. Similarly, we’ll use an array for each event type, so that we can bind multiple event listeners to each event type. I’ll also add a simple function which registers event listeners while I’m at it.

function emitter (thing) {
  var events = {};

  if (!thing) {
    thing = {};
  }

  thing.on = function (type, listener) {
    if (!events[type]) {
      events[type] = [listener];
    } else {
      events[type].push(listener);
    }
  };

  return thing;
}

So far so good, now you can add event listeners, once an emitter is created. This is how it’d work. Keep in mind that listeners can be provided with an arbitrary number of arguments, when an event is fired, and we’ll implement the method to fire events next.

var thing = emitter();

thing.on('change', function () {
  console.log('thing changed!');
});

Naturally, that works just like a DOM event listener. All we need to do now is implement the method which fires the events. Without it, there wouldn’t be an event emitter. I’ll implement an emit method which allows you to fire the event listeners for a particular event type, passing in an arbitrary number of arguments. Here is how it’d look like.

thing.emit = function (type) {
  var evt = events[type];
  if (!evt) {
    return;
  }
  var args = Array.prototype.slice.call(arguments, 1);
  for (var i = 0; i < evt.length; i++) {
    evt[i].apply(thing, args);
  }
};

The Array.prototype.slice.call(arguments, 1) statement is an interesting one. Here I’m apply Array.prototype.slice on the arguments object, and telling it to start at index 1. This does two things for me. It casts the arguments object into a true array, and it gives me a nice array with all of the arguments that were passed into emit, except for the event type, which I don’t need to invoke the event listeners.

There’s one last tweak I’d like to do, which is executing the listeners asynchronously, so that they don’t halt execution of the main loop if one of them blows up. You could also use a try catch block here, but I’d rather not get involved with exceptions in event listeners, let the consumer handle that. To achieve this, I’ll just use a setTimeout call, as shown below.

thing.emit = function (type) {
  var evt = events[type];
  if (!evt) {
    return;
  }
  var args = Array.prototype.slice.call(arguments, 1);
  for (var i = 0; i < evt.length; i++) {
    debounce(evt[i]);
  }
  function debounce (e) {
    setTimeout(function () {
      e.apply(thing, args);
    }, 0);
  }
};

You should now be able to create emitter objects, or you can also turn existing objects into event emitters. Note that, because I’m debouncing the event listeners, if an event throws the rest will still run to completion. This is not always the case in other implementations of events.

Emitters inside contra

If you check out the documentation for contra you’ll find out that the interface to interact with λ.emitter is basically the same as what I’ve just explained. In addition to the on() and emit() methods, the implementation in contra offers a once() method which would register an event handler that should only trigger once, and an off() method which can turn off any listener, including those registered by once(), all while staying around 30 lines of code!

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments (9)

phil wrote

Not that long ago (a year or two), I would have preferred the pseudo-class hierarchy that so many try to force onto JavaScript.

Now, I much prefer methods like you’ve got above, basically mixins (and duck-typing) wherever possible. I pretty much avoid the new keyword in my code when it makes sense.

Marco Faustinelli wrote
Marco Faustinelli wrote

Prototypes enforce limitations for little gain, and that’s what we avoid by going around it and merely adding methods on existing objects, without any prototypal inheritance going on.

Could we please engrave in some kind of eternal digital stone these words?

Your book rocks, keep up the good work…

Sandeep Kumar Patel wrote

Hi Nicolas,I like your blog.It is awesome. Can you please tell me which Blogging Platform you have used to create this blog.

Nicolas Bevacqua wrote

My own!

That being said, it’s going intense refactoring, so you might want to wait a few months for me to complete the refactor before jumping ship…

Stephen Band wrote

Your emit function calls the actual array of listeners, and if one of your listeners happens to modify the array of listeners (by calling .on or .off or so on), that can lead to a hard-to-detect class of errors, where listeners are being omitted and you’re not sure why.

The array of listeners should be cloned before you loop over it.

var evt = events[type];

if (evt) {
    evt = evt.slice();
}
else {
    return;
}