Let's Make a Framework: Custom Events

30 Jun 2011 | By Alex Young | Tags frameworks tutorials lmaf

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 as on: Register an event handler
  • emit(event, [arg1], [arg2], [...]): Emit an event with optional arguments
  • removeListener(event, fn): Remove the handler
  • removeAllListeners(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!

References


blog comments powered by Disqus