Let's Make a Framework: Custom Events 2

07 Jul 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.

Last week I looked at how Node supports event handling with EventEmitter. This tutorial explores building a simple event manager that can be used for custom event handling.

Event Emitter API

This is the API I settled on in the previous part:

  • 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): Remove all handlers for the specified event

A skeleton prototype class will look like this:

  Emitter = function() {
    this.events = {};
  };

  Emitter.prototype = {
    /**
     * Adds a listener.  Multiple can be added per eventName.  Aliased as `on`.
     *
     * @param {String} eventName The name of the event
     * @param {Function} handler A callback
     */
    addListener: function(eventName, handler) {
    },

    /**
     * Triggers all matching listeners.
     *
     * @param {String} eventName The name of the event
     * @returns {Boolean} `true` if an event fired
     */
    emit: function(eventName) {
    },

    /**
     * Removes all matching listeners.
     *
     * @param {String} eventName The name of the event
     * @returns {Boolean} `true` if an event was removed
     */
    removeAllListeners: function(eventName) {
    },

    /**
     * Removes a listener based on the handler function.
     *
     * @param {String} eventName The name of the event
     * @param {Function} handler The handler function to remove
     * @returns {Boolean} `true` if an event was removed
     */
    removeListener: function(eventName, handler) {
    }
  };

  Emitter.prototype.on = Emitter.prototype.addListener;

I’ve added code comments in at this stage because I wanted to think about the return values and parameters. I wasn’t sure if most of these methods should return anything at all, but I decided returning some kind of status would make testing a lot easier.

Unit Test

Adding listeners and emitting events can be tested together:

exports.testEventEmitter = {
  'test addListener': function() {
    var Emitter = turing.events.Emitter,
        emitter = new Emitter(),
        i = 0;
    
    emitter.on('eventName', function() {
      i++;
    });

    emitter.on('testFired', function() {
      assert.equal(i, 1);
    });

    assert.ok(emitter.emit('eventName'));
    assert.ok(emitter.emit('testFired'));
  },

A counter is used to determine if an event has fired, and each method’s return value is tested.

Removing all listeners is another simple case:

  'test removeAllListeners': function() {
    var Emitter = turing.events.Emitter,
        emitter = new Emitter();
    
    emitter.on('event1', function() {});
    emitter.on('event2', function() {});

    emitter.removeAllListeners('event1');
  
    assert.ok(!emitter.emit('event1'));
    assert.ok(emitter.emit('event2'));
  },

Removing an event then trying to emit returns false, which helps make these tests fairly readable.

Removing a specific listener is a little bit more complicated. Multiple handlers can be added per event name, so we need to ensure this is managed correctly. You’ll also notice I use functions instead of anonymous functions for these tests, that’s just how removeListener has to work.

  'test removeListener': function() {
    var Emitter = turing.events.Emitter,
        emitter = new Emitter(),
        i = 0;
    
    function add() {
      i++;
    }

    function nothing() {
    }

    emitter.on('add', add);
    emitter.on('add', nothing);
    emitter.on('testFired', function() {
      assert.equal(i, 1);
    });

    assert.ok(emitter.emit('add'));
    assert.ok(emitter.removeListener('add', add));
    assert.ok(emitter.emit('add'));
    assert.ok(emitter.removeListener('add', nothing));
    assert.ok(!emitter.removeListener('add', nothing));
    assert.ok(!emitter.emit('add'));
    assert.ok(emitter.emit('testFired'));
  }
};

Notice how the boolean return values are useful here as well.

Implementation

I’ve removed the comments from these examples to make this more readable in this tutorial.

Adding a listener is pretty easy. We have a simple JavaScript Object containing events indexed by name. Each item is an array of event handler functions.

Emitter.prototype = {
  addListener: function(eventName, handler) {
    if (eventName in this.events === false)
      this.events[eventName] = [];

    this.events[eventName].push(handler);
  },

If you’re confused about the eventName in this.events syntax it’s just a simple test to see if the property is present in the internal list of events. Just think about it like this:

'a' in { a: 1, b: 2 }
// true

'c' in { a: 1, b: 2 }
// false

Emitting an event uses this to avoid adding an extra layer of indentation, and then loops through each matching handler and runs it:

  emit: function(eventName) {
    var fired = false;
    if (eventName in this.events === false) return fired;

    for (var i = 0; i < this.events[eventName].length; i++) {
      this.events[eventName][i].apply(this, Array.prototype.slice.call(arguments, 1));
      fired = true;
    }
    return fired;
  },

Removing all listeners is fairly simple too, I just use delete to remove the property from the list of events:

  removeAllListeners: function(eventName) {
    if (eventName in this.events === false) return false;
    
    delete this.events[eventName];
    return true;
  },

Removing single events is probably the hardest part. For a start, we need to remember to pass in the original function. It’s often more convenient to use an anonymous callback for this style of API, so this usually confuses people at first.

Then the handler has to actually be removed from the array. I’d usually use slice and indexOf, but as I’m building this with browsers in mind I’ve used a helper method called removeListenerAt which is really just JavaScript Array Remove by John Resig.

This method chops an array into two parts based on the index of the item we want to remove, found using a for loop. The original array is split one position after the item we want to remove. Then the original array’s length is shortened, removing the item we don’t need. Finally, the two halves are joined back together using push.

  removeListenerAt: function(eventName, i) {
    var array = this.events[eventName],
        rest = array.slice(i + 1);
    array.length = i;
    array.push.apply(array, rest);
    this.events[eventName] = array;
  },

  removeListener: function(eventName, handler) {
    if (eventName in this.events === false) return false;

    for (var i = 0; i < this.events[eventName].length; i++) {
      if (this.events[eventName][i] == handler) {
        this.removeListenerAt(eventName, i);
        return true;
      }
    }

    return false;
  }
};

Conclusion

I hope this demonstrates how simple event handling really is. I’ve avoided some of the optimisations from EventEmitter in Node to keep the code easy to follow.

The full version of this code can be found in commit 1e9f11d of turing.js.

References


blog comments powered by Disqus