Let’s take a shot at controlling the order of execution of synchronous and asynchronous functions in JavaScript.
If you want to play along instead of just reading passively, fire up a JavaScript Shell and copy/paste all code directly into it.
We’ll start off with everyone’s favorite annoyance. Popup alerts.
var sayA = function() {
alert("a");
}
var sayB = function() {
alert("b");
}
So, let’s say we want to run those in order. First sayA, then sayB and then sayA again.
var funcArray = [sayA, sayB, sayA];
for (var i=0;i<funcArray.length;i++) {
funcArray[i]();
}
Nice and cuddly. But that is, of course, no fun at all. Time to start making this a little more interesting.
Let’s pretend we want to grab a random number from an asynchronous request.
var asyncGetNumber = function(request) {
window.setTimeout(
function() { request.handler(Math.floor(Math.random()*16)); }
, 500);
}
And let’s have a say function that alerts the returned value..
sayRandom = function() {
var request = {
handler: function(n) {
alert(n);
}
};
asyncGetNumber(request);
}
And before we start repeating ourselves, let’s have a function that runs functions for us.
var sequence = function(funcArray) {
for (var i=0;i<funcArray.length;i++) {
funcArray[i]();
}
}
Now, let’s rock!
sequence([sayRandom,sayA,sayB]);
Oops. Notice what happened? Our alerts in order were “a”, “b” and here in my case “5″. Not the order we wanted.
As the handler function in asyncGetNumber falls outside of the execution sequence, we can’t control the execution order. Time to change that. Enter action sequences(tm) (get yours free with an extra large order of fries).
Now we’re going to use a few features of JavaScript that are generally speaking less well understood than, say, alert.
We’re going to create and pass around functions on the fly. First we re-write sayA, sayB and sayRandom to accept a handler parameter. This is basically a function that it’ll call when it’s done doing whatever it does. (How’s that for non-specific?)
For sayA and sayB this is pretty trivial.
var sayA = function(handler) {
alert("a");
handler && handler();
};
var sayB = function(handler) {
alert("b");
handler && handler();
};
Not very spectacular, right? Don’t worry – we’re not there yet.
A detail to note is the use of,
handler && handler();
The && works as a guard operator, giving these functions the new handler functionality if a parameter is passed (by executing it), but otherwise operating exactly as they did before.
sayRandom = function(handler) {
var request = {
handler: function(n) {
alert(n);
handler && handler();
}
};
asyncGetNumber(request);
}
The handler parameter is available in the local scope, which includes the scope of the function found at the handler key of the request object. So when request.handler is executed, the handler parameter passed to sayRandom is available to it.And now we re-write sequence as follows,
var sequence = function(funcArray) {
// runFunctions is a function that takes the first element of the funcArray
// and executes it with *itself* as the parameter
var runFunctions = function() {
// If there is anything left in the Array
if (funcArray.length != 0) {
// Take it out and assign it to a local variable
var latestFunction = funcArray.shift();
// Execute it and call with ourself
latestFunction(runFunctions);
}
}
// Now run it.
runFunctions();
};
sequence([sayRandom,sayA,sayB]);
Voila. Sequenced actions.
It still looks a little crufty, though.
Let’s clean that up a little. For this we’ll use two aspects of functions in JavaScript. The first being that we can anonymously define and evaluate a function on the fly. The second being that a function in JavaScript can refer to itself, using the local variable arguments.callee.
We’ll use these to create a new local scope that can be referred to with arguments.callee, but which has access to the funcArray parameter passed to sequence.
A direct evaluation looks like this,
(function(){
alert("How you doin'?");
})();
And combining this with arguments.callee allows you to quickly and efficiently shoot yourself in the foot by blowing up your stack ..
(function(){ arguments.callee(); })(); // Keep calling ourselves
Now, applying this to a re-write of sequence..
var sequence = function(funcArray) {
(function() {
(funcArray.length != 0) &&
funcArray.shift()(arguments.callee);
})();
};
Much more elegant. Now, let’s use that to get 10 random numbers.
var actions = [];
for (var i=0; i<10; i++) {
actions.push(sayRandom);
}
sequence(actions);
10 random numbers in 5 seconds, if you click really fast.