Let's Make a Framework: Custom Events
Let’s Make a Framework is an ongoing series about building a JavaScript framework from the ground up.
These articles are tagged with lmaf. The project we’re creating is called Turing. Documentation is available at turingjs.com.
Way back at the start of this series I created a basic DOM events implementation, in Let’s Make a Framework: Events. Dealing with browser events is mostly a case of providing a simple API and patching browser quirks. As we saw, various frameworks implement this differently. In particular, jQuery has an almost completely custom event handling system, which allows it to support lots of useful features in almost every browser.
When building modern web applications we often need to deal with events that don’t apply to DOM elements. Sometimes we create abstract objects and want to emit or listen for events. This is becoming more common with the growing popularity of Node and its EventEmitter class.
Let’s add something like EventEmitter to Turing, so developers can make use of customised event handling for their own objects.
EventEmitter
The EventEmitter class can be found in lib/events.js in Node and there’s also documentation in events.EventEmitter.
It’s actually very easy to use EventEmitter. I think a lot of people find it confusing at first, because they hear about “evented JavaScript” and think Node’s little EventEmitter is a magical tool that makes everything fast.
In reality, it’s basically this:
var EventEmitter = require('events').EventEmitter,
bomb = new EventEmitter();
bomb.on('explode', function() {
console.log('BOOM!');
});
bomb.emit('explode');
This is similar to what CommonJS Events/A describes.
Structure
In EventEmitter, event handlers are stored in an object that contains arrays of events indexed by the event names. Some additional complexity comes from optimisations. For example, one optimisation is for the case where there’s only one listener:
EventEmitter.prototype.addListener = function(type, listener) {
// ...
if (!this._events[type]) {
// Optimize the case of one listener. Don't need the extra array object.
this._events[type] = listener;
} else if (isArray(this._events[type])) {
// If we've already got an array, just append.
this._events[type].push(listener);
Then, when an event is emitted:
EventEmitter.prototype.emit = function(type) {
if (!this._events) return false;
var handler = this._events[type];
if (!handler) return false;
if (typeof handler == 'function') {
// The optimised case
switch (arguments.length) {
// fast cases
case 1:
handler.call(this);
// ... snip
} else if (isArray(handler)) {
// An array of handlers has been added
var listeners = handler.slice();
for (var i = 0, l = listeners.length; i < l; i++) {
listeners[i].apply(this, args);
}
return true;
API Design
Let’s follow the basics of Node’s API:
addListener(event, fn), aliased ason: Register an event handleremit(event, [arg1], [arg2], [...]): Emit an event with optional argumentsremoveListener(event, fn): Remove the handlerremoveAllListeners(event): Removes all handlers for the event
These methods should provide enough functionality to do some cool stuff.
Conclusion
Whether we’re using events in DOM programming, EventEmitter in Node, or even similar techniques in other languages (I keep finding myself working with NSNotification in Objective-C), events are an incredibly useful paradigm.
Next week I’ll start building our own events class!
