Creating an Events Mixin

6/19/14

Events are commonplace in JavaScript, and an event mixin is useful for structuring an appplication by decoupling how modules communicate. The mixin can be used to give a module event functionality or to create an app level publish and subscribe mechanism. Event functionality is often available in some capacity in many libraries, for example, jQuery provides custom event support and Backbone’s event mixin is available as part of all collections, models, and views. I made a post regarding the revealing module pattern which mentions the EventMixin, and here I'll detail the implementation.

The basic event mixin interface looks something like this:

      
function EventsMixin () {
  var events = {};

  function on (eventName, callback, context) {}
  function trigger (eventName) {}
  function off (eventName) {}

  return function () {
    this.on = on;
    this.trigger = trigger;
    this.off = off;

    return this;
  };
}
      
    

A revealing module pattern fits well with keeping tracked events accessible only by internally defined functions. If another module wants to interact with stored events, I prefer it do so through the interface provided. As for each of the exposed module functions, a short description of what each does:

on (eventName, callback, context) : registers a callback by name to be executed at the given context.

trigger (eventName) : execute all registered callbacks at the given name, at their stored contexts.

off (eventName) : removes all registered events at the given name.

To use the events mixin, create a new constructor for each module that will be mixed and call the constructor with the module as the 'this' context. The constructor will create references on the specified module.

      
   // with a module that just returns an object
   var myModule = MyModule();
   EventsMixin().call(myModule);

   // with a module that has a constructor and uses new
   function MyModule () {
     EventsMixin().call(this);
   }
   var myModule = new MyModule();

   myModule.on('name', function () { });
   myModule.trigger('name');
   myModule.off('name');
      
    

Now the implementation details, starting with .on(). I need a way to track events when added. I’ll use an object to store callback references by name, and since I probably need to support multiple callbacks for the same event, I'll want to store the callbacks and their relative context in an array. Property access by bracket notation is great for this scenario.

      
  var events = {};
  function on (eventName, callback, context) {
   // if a reference for callbacks for this
   // event name does not exist yet, create it
   if (!events[eventName]) {
     events[eventName] = [];
   }

   events[eventName].push({
     callback: callback,
     context: context
   });
  }
      
    

Now that the callbacks are being stored at arbitrary event names, I need a way to trigger them. In order to make the design flexible I want to support an arbitrary number of additional arguments that can be passed to my event callback. I’ll make use of Array.prototype.slice() and the arguments object to manage args, and apply() to execute our callbacks at the provided scope with the args.

      
  var events = {};
  function trigger (eventName) {
    // get all args after the first
    var args = Array.prototype.slice.call(arguments, 1);
    var callbacks = events[eventName];
    var length = callbacks.length;
    var stored;

    for (var i = 0; i < length; i++) {
      stored = callbacks[i];
      stored.callback.apply(stored.context, args);
    }
  }
      
    

Lastly, the ability to remove events from the module is also handy. I can clear or reset the array a few different ways, splice is one option, but simply setting our reference to a new array should be faster, and gets the job done.

      
  var events = {};
  function off (eventName) {
    events[eventName] = [];
  }
      
    

The events mixin is now complete. You can find a gist with the full working implementation on github, and below are some related resources. Feedback and suggestions welcome!

comments powered by Disqus