…or, “Reinventing the while” ;)
JavaScript in a web browser window is an event-driven, but single-threaded environment. When a JavaScript code unit starts executing, it has the undivided attention of the browser window until it’s finished executing. When such an atomic action requires so much time that the user perceives this unresponsiveness, the popular solution is to divide that action into smaller pieces and perform the pieces asynchronously, using setTimeout or setInterval.
This is simple when all the pieces are essentially the same: when you can just iterate through them and do them one after another. But when the task is a little more complex, with nested loops and conditional logic, the splitting up of that task becomes a lot more complex. I’ve written some sugar that factors out nearly all of the code required for managing the program’s asynchronicity, allowing your code to look more natural.
(You can download the sugar now, or read through to the end.)
A Bad Example
Let’s say I wanted to insert 100,000 exclamation points at the bottom of the page. A simple for loop does the trick, but like all great things, it takes some time to complete:
function write(s) {
// Add a string to the bottom of the page.
document.body.appendChild(document.createTextNode(s));
}
for (var i = 0; i < 100000; i++) {
write('! '); // include a space, for wrapping
}
write('done!');
To make this asynchronous, the obvious and direct way is something like this:
var i = 0;
var interval = setInterval(function(){
if (i < 100000) {
write('! ');
i++;
} else {
clearInterval(interval);
write('done!');
}
}, 0);
That’s fine for making a single loop asynchronous, but if I have nested loops with conditional logic, the necessary code becomes a lot less straightforward. Take this loop for example (with smaller numbers this time for sanity’s sake), and suppose I want to make every write() invocation happen asynchronously:
var x, y;
for (x = 0; x < 10; x++) {
write('x = '+x);
if (x > 5) {
for (y = 13; y > 10; y--) {
write('first y = '+y);
}
for (y = 0; y < 3; y++) {
write('second y = '+y);
}
}
}
write('done');
That means we can’t just make the outer loop asynchronous and be done with it. Instead, it would end up looking something like this:
var x, y, interval, interval2, waiting = false;
x = 0;
interval = setInterval(function(){
if (!waiting) {
if (x < 10) {
write('x = '+x);
if (x > 5) {
waiting = true;
y = 13;
interval2 = setInterval(function(){
if (y > 10) {
write('first y = '+y);
y--;
} else {
clearInterval(interval2);
y = 0;
interval2 = setInterval(function(){
if (y < 3) {
write('second y = '+y);
y++;
} else {
clearInterval(interval2);
waiting = false;
}
}, 0);
}
}, 0);
}
x++;
} else {
clearInterval(interval);
write('done');
}
}
}, 0);
What a mess. So much of the code is devoted to making it asynchronous, that it has become difficult to look at the program and understand what it does.
Asynchronizer
Here’s where the sugar comes in. The Asynchronizer constructor creates an object that models the looping control structures we’re familiar with: for and while.
Modeling a for Loop
The first example from above is good for demonstrating the for_() method:
var a = new Asynchronizer();
var i;
a.for_(
function(){i = 0;},
function(){return i < 100000;},
function(){i++;},
function($){
write('! ');
$.continue_();
},
function(){
write('done!');
}
);
It looks a little odd, being so function-heavy, but from the way the code is formatted, you can see what it’s intended to do. It takes five arguments, each of which is a function which represents one part of the loop.
init- This function is called once when the loop begins.
test- This function is called before every iteration of the loop. If it returns true, the
statementsfunction is called. If it returns false, the loop ends. inc- This function is called after every iteration of the loop.
statements-
This function contains the body of the loop, and is called every time the
testfunction returns true. callback- This function is called when the loop is complete.
The Asynchronizer object will pass an object to the statements function you supply. That object has two methods–break_() and continue_()–which mimic the break and continue JavaScript keywords.
The continue_() method marks the end of an iteration, and the loop will not continue until it is invoked. This requirement enables us to place other asynchronous actions inside the loop, as we’ll see in the next example.
We can make a simple nested loop, then asynchronize it:
// synchronous:
var x, y;
for (x = 0; x < 10; x++) {
for (y = 0; y < 10; y++) {
write(x + ', ' + y);
}
}
write('done');
// asynchronous:
var a = new Asynchronizer();
var x, y;
a.for_(
function(){x = 0;},
function(){return x < 10;},
function(){x++;},
function($){
a.for_(
function(){y = 0;},
function(){return y < 10;},
function(){y++;},
function($){
write(x + ', ' + y);
$.continue_();
},
$.continue_
);
},
function(){
write('done');
}
);
Note the difference in how the continue_ method is called in each loop. The operation in the body of the inner loop is synchronous (write), and so continue_() is called explicitly, with parentheses. But the operation in the body of the outer loop is asynchronous (another for_ invocation), and so instead, continue_ is used as the callback for the inner loop, not to be invoked until the loop is finished.
Thanks to JavaScript’s closures, the value of the $ argument to each statements function persists after the function has returned, allowing the inner loop to use it correctly as its callback.
Modeling while
As you can guess, the while_() method models a while loop, but otherwise works the same way as the for_() method:
var a = new Asynchronizer();
var i = 0;
a.while_(
function(){return i < 50;},
function($){
write(i);
i++;
$.continue_();
},
function(){
write('done!');
}
);
Chaining Asynchronous Operations
The chain_() method allows a sequence of asynchronous operations to be performed each after the previous one completes. It takes one or more arguments, all of which must be functions. The last function in the chain can be thought of as the callback.
The Asynchronizer passes to each function except the last one, an object identical to the one described in the section about for_() above. When that object’s continue_ method is invoked, the next function in the chain is invoked. If the object’s break_ method is invoked, the last function in the chain is invoked.
Here’s the second example from above, rewritten with Asynchronizer, illustrating the use of chain_(). The second y loop is started only after the first is complete:
var a = new Asynchronizer();
var x, y;
a.for_(
function(){x = 0;},
function(){return x < 10;},
function(){x++;},
function($){
write('x = '+x);
if (x > 5) {
a.chain_(
function($){
a.for_(
function(){y = 13;},
function(){return y > 10;},
function(){y--;},
function($){
write('first y = '+y);
$.continue_();
},
$.continue_
);
},
function($){
a.for_(
function(){y = 0;},
function(){return y < 3;},
function(){y++;},
function($){
write('second y = '+y);
$.continue_();
},
$.continue_
);
},
$.continue_
);
} else {
$.continue_();
}
},
function(){write('done');}
);
Also notice the conditional way in which continue_ is invoked in the outer loop. If the asynchronous chain_ is to be performed, continue_ is used as the callback. Otherwise, it gets invoked explicitly.
External Control Methods
An Asynchronizer object has five additional methods, which allow outside control over the execution of the loops it models.
pause()- Pauses execution until
resume()is invoked. resume()- Resumes execution after it has been paused.
paused()- Returns
trueif theAsynchronizeris paused, orfalseif it isn’t. running()- Returns
trueif theAsynchronizerhas functions queued and waiting to be invoked, orfalseif it doesn’t. initialize()- Resets the
Asynchronizerto a non-paused, non-running state. Any functions in queue are discarded.
The Sugar
You can download the code that provides the Asynchronizer sugar here: Asynchronizer.js
New Version
I’ve added new features in response to drewp’s comment below. Read the next article, Clumping and More.
2 Responses
Looks cool, but how about a shorter 'for x in y' equivalent? In many cases, I have one iterable thing and one function to map over it. I shouldn't need to produce 5 functions for a case like that. Prototypejs calls it "foreach", which might be a good name here.
How's the overhead of calling setTimeout on each element? If I know that my loop body is sufficiently cheap, would it be practical for me to say "run these in chunks of 10"? Could you figure out the chunk size dynamically?
These forms would connect pretty naturally to mochikit's deferred callback system.
It would also be awesome if you could transparently move the loop body to a gears WorkerPool (http://code.google.com/apis/gears/api_workerpool.html) if gears is installed, although I could imagine there are some pretty big constraints on that.
Emulating for...in shouldn't be too hard. I'd probably use a real for...in loop to gather the property names, and then iterate through them using Asynchronizer's for_ method. I'll probably add that feature soon.
Putting each loop iteration into the event queue separately does have serious performance problems, so I've actually started working on a way of making partially synchronous loops, where you specify on the outside how many iterations should be done in a single dispatch. This will hopefully be in the next version too.
You're on your own with Mochikit and WorkerPool. ;)