This post is part of a series on event-driven programming. The complete series is:
- Events: they’re not just for the DOM, you know
- Observable objects
- Deferrable values
- Asynchronous methods
- First-leg round-up and final remarks
- Object lifecycle
- Asynchronous pipelines
- Testing event-driven apps
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.