DailyJS

Let's Make a Framework: Custom Events 2

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials frameworks lmaf

Let's Make a Framework: Custom Events 2

Posted by Alex R. Young on .
Featured

tutorials frameworks lmaf

Let's Make a Framework: Custom Events 2

Posted by Alex R. Young on .
*Let's Make a Framework* is an ongoing series about building a JavaScript framework from the ground up. These articles are tagged with [lmaf](http://dailyjs.com/tags.html#lmaf). The project we're creating is called [Turing](http://github.com/alexyoung/turing.js). Documentation is available at [turingjs.com](http://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