Code Review: EventEmitter2
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
manymethod, which is similar toonce - 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.