Evented programming patterns: Object lifecycle

This post is part of a series on event-driven programming. The complete series is:

Earlier in this series I covered a very common pattern in event-driven programming: the Observable object. This technique lets one object notify many others when interesting things happen. JavaScript developers will be very familiar with this: it’s the same pattern that underlies the DOM event model.

I while ago I rewrote the JS.Class package loader and noticed a variation of this pattern emerge, which I’m going to call the object lifecycle. The typical use case is when some part of your code needs to execute once, as soon as some condition becomes true. In the package loader, this looks something like:

thePackage.when('loaded', function() {
  // Run code that relies on thePackage
});

This says: if thePackage is already loaded, then run this callback immediately. Otherwise, wait until thePackage is loaded and then run the callback. The implication is that the package will become loaded, only once, at some point in its life, and as soon as that happens we want to be notified. (I tend to use when for one-shot lifecycle events, and on for multi-fire events.) The implementation is quite similar to the Observable pattern, so you might want to revisit that before reading on.

Obviously, our lifecycle object is going to need to store lists of callbacks, indexed by event name as before. But in this case, if we know the event has already been triggered on that object, we can run the callback immediately and forget about it. When we trigger events, we also want to remove all the old pending callbacks after running them, since they don’t need to be called again.

LifeCycle = {
  when: function(eventType, listener, scope) {
    this._firedEvents = this._firedEvents || {};
    if (this._firedEvents.hasOwnProperty(eventType))
      return listener.call(scope);

    this._listeners = this._listeners || {};
    var list = this._listeners[eventType] = this._listeners[eventType] || [];
    list.push([listener, scope]);
  },
  
  trigger: function(eventType) {
    this._firedEvents = this._firedEvents || {};
    
    if (this._firedEvents.hasOwnProperty(eventType)) return false;
    this._firedEvents[eventName] = true;
    
    if (!this._listeners) return true;
    var list = this._listeners[eventType];
    if (!list) return true;
    list.forEach(function(listener) {
      listener[0].apply(listener[1], args);
    });
    delete this._listeners[eventType];
    return true;
  }
};

Note how the trigger() method checks to see if the event has already been fired: we don’t want the same stage in the lifecycle to be triggered multiple times. It also removes the listeners from the object after calling them, and returns true or false to indicate whether the event fired. This makes it easy to tell whether some action that should only be done once has already happened; for example in my package system I do something like this:

JS.Package = new JS.Class({
  include: LifeCycle,
  
  // various methods
  
  load: function() {
    if (!this.trigger('request')) return;
    // perform download logic...
  }
});

This kills two birds with one stone: it lets other listeners know that the package has been requested and checks whether it’s already been requested, so we don’t try to download the package multiple times.

Naturally, one thing a package system has to deal with is dependencies. Dependencies are just prerequisites: you can’t load a package until all its dependencies are loaded. More precisely, a package is loaded once the browser has downloaded its source code, and it is complete once it is loaded and all its dependencies are complete. To fill in some more of the load() method, this is easily expressed as:

JS.Package.prototype.load = function() {
  if (!this.trigger('request')) return;
  
  when({complete: this._dependencies, load: [this]}, function() {
    this.trigger('complete');
  }, this);

  when({loaded: this._dependencies}, function() {
    loadFile(this._path, function() { this.trigger('load') }, this);
  }, this);
};

This reads quite naturally: if the package has already been requested, do nothing. When the dependencies are complete and this package is loaded, then this package is complete. When the dependencies are loaded, load this package and then trigger its load event. Note how the load event will trigger a complete event if there are no dependencies, and this will ripple down the tree and trigger dependent packages to load.

I’ve used when() above to express groups of prerequisites in a natural way, but we don’t have an implementation for that yet – we only have the when() method for individual objects. So let’s write one. This when() function will need to gather up the list of preconditions, keep a tally of how many have triggered, and when they’re all done we can fire our callback. The first step in the function converts preconditions, which maps event names to lists of objects, into a simple list of object-event pairs. That is it turns {complete: [foo, bar], load: [this]} into [[foo, 'complete'], [bar, 'complete'], [this, 'load']].

var when = function(preconditions, listener, scope) {
  var eventList = [];
  for (var eventType in preconditions) {
    for (var i = 0, n = preconditions[eventType].length; i < n; i++) {
      var object = preconditions[eventType][i];
      eventList.push([object, eventType]);
    }
  }
  
  var pending = eventList.length;
  if (pending === 0) return listener.call(scope);
  
  for (var i = 0, n = pending; i < n; i++) {
    eventList[i][0].when(eventList[i][1], function() {
      pending -= 1;
      if (pending === 0) listener.call(scope);
    });
  }
};

If there are no pending events, we can just call the listener immediately. Otherwise, we set up listeners for all the events, and when each one fires (and remember: some of them may have fired already) we count down how many events we’re waiting for. When this reaches zero, we can carry on with the work we wanted to do.

This pattern is essentially a cross between Observable and Deferrable: we’re deferring an action, but the deferred items – the events – aren’t complex enough to merit their own objects so the implementation is closer to an observable object. The technique lends itself really well to expressing prerequisites in a natural way, even if the work you’re doing is not asynchronous.

I’ll have a couple more articles on event-driven programming next week, and you can catch me speaking at the London Ajax User Group on August 10th where I’ll be talking about Faye, event-driven code and testing.