DailyJS

Code Review: EventEmitter2

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

events node code-review

Code Review: EventEmitter2

Posted by Alex R. Young on .
Featured

events node code-review

Code Review: EventEmitter2

Posted by Alex R. Young on .
*Code Review* is a series on DailyJS where I take a look at an open source project to see how it's built. Along the way we'll learn patterns and techniques by JavaScript masters. If you're looking for tips to write better apps, or just want to see how they're structured in established projects, then this is the tutorial series for you.

A few weeks ago I was working on the deceptively simple problem of
writing a custom events library for the Framework series in Custom
Events
. The
EventEmitter2 module is an interesting alternative to Node's EventEmitter, which adds a few novel
features to this problem space.

About EventEmitter2

EventEmitter2 (License: MIT, npm: eventemitter2) by hij1nx is an alternative to EventEmitter that adds several unique features:

  • Namespaces
  • Wildcards
  • A many method, which is similar to once
  • Browser compatibility
  • Performance improvements over EventEmitter

Usage

Usage is basically the same as
EventEmitter. The constructor takes a configuration object, where the namespace
delimiter can be changed:

var server = EventEmitter2({
    wildcard: true
  , delimiter: '::'
  , maxListeners: 20
});

The many method sounds a little bit confusing at first, but
all it does is fires an event several times and then removes it:

server.many('quad hello', 4, function() {
  console.log('hello');
});

Structure

EventEmitter2 is distributed as a Node module with a detailed
package.json and tests. The main library file,
lib/eventemitter2.js contains all of the source in a self-executing anonymous function so
browsers can be catered for:

;!function(exports, undefined) {
  // source goes here

  exports.EventEmitter2 = EventEmitter;
}(typeof exports === 'undefined' ? window : exports);

An inArray method is also defined for browser support:

var isArray = Array.isArray ? Array.isArray : function _isArray(obj) {
  return Object.prototype.toString.call(obj) === "[object Array]";
};

Other than that the library's overall structure should look familiar to
anyone who's heavily used EventEmitter.

Configuration

I thought it was interesting how configuration is removed from the
constructor:

function configure(conf) {

  if (conf) {
    this.wildcard = conf.wildcard;
    this.delimiter = conf.delimiter || '.';

    if (this.wildcard) {
      this.listenerTree = new Object;
    }
  }
}

function EventEmitter(conf) {
  this._events = new Object;
  configure.call(this, conf);
}

Presumably this is to differentiate between constructor initialisation
and setting configuration value defaults.

Similarities to EventEmitter

I noticed a few familiar techniques from EventEmitter, like this
optimisation from when events are added:

if (!this._events[type]) {
  // Optimize the case of one listener. Don't need the extra array object.
  this._events[type] = listener;
}

And the listener leak protection is the same too:

// Check for listener leak
if (!this._events[type].warned) {

var m;
if (this._events.maxListeners !== undefined) {
  m = this._events.maxListeners;
} else {
  m = defaultMaxListeners;
}

if (m && m > 0 && this._events[type].length > m) {

  this._events[type].warned = true;
  console.error('(node) warning: possible EventEmitter memory ' +
                'leak detected. %d listeners added. ' +
                'Use emitter.setMaxListeners() to increase limit.',
                this._events[type].length);
  console.trace();
}

Wildcard Support

When wildcard support is enabled, two methods are used:
searchListenerTree and growListenerTree.
EventEmitter2 uses an object to hold "branches" for namespaces so they
can be recursively searched.

When adding an event, growListenerTree is called with the
event name and the listener function. The event name is
split based on the configured delimiter. Each part of the
event name is removed from an array, and a tree is built up as required.
The leaves are the listener functions.

Just like the EventEmitter optimisation, if there's only one handler
it's stored as a function, else an array of functions is used. The
growListenerTree function will return true,
but the return value is never used.

Tests

The tests include a benchmark to compare against
EventEmitter, and this uses the
Benchmark.js library that we seem to keep referencing on DailyJS lately. The rest of the tests are split into
"simple" and "wildcardEvents", because EventEmitter2 uses very different code if wildcards are required.

These tests use good ol' nodeunit,
and use test.expect to ensure the correct number of
assertions are run per-test.

Conclusion

Comparing EventEmitter2 to the original EventEmitter is interesting,
because it shows how adding a seemingly simple feature like namespaces
requires a very different approach.

The original thread on the nodejs group where hij1nx came up with
EventEmitter2 is here: Namespaced
EventEmitters?
,
and you can see the comments that influenced the fundamental separation
of simple and wildcard/namespaced events.