Queueing timeouts in JavaScript

22 September 2006 (programming XML)

I wanted to create a simple animation to illustrate sorting algorithms, using SVG and JavaScript. The basic idea was to present a simple JavaScript API that sorting implementations could use. It turned out I had to implement my own timeout queueing contraption for this.

I was aiming for being able to write something like the following code:

function bubble ()
{
    for (var i = array.length - 1; i != 0; --i)
    {
        for (var j = 0; j != i; ++j)
        {
            if (less (j + 1, j))
                swap (j + 1, j);
        }
        mark_as_done (i);
    }
    mark_as_done (0);
}

Looks pretty straight-forward, doesn't it? One just needs to write less(), swap() and mark_as_done() to change/animate certain SVG objects, and it's all done, right? Well, I'm certainly no Javascript wizard, so that's what I thought too.

However, since JavaScript is not run in a thread separate from the browser, you can't just implement animation by modifying values and wait()'ing for a couple of milliseconds. In fact, it turns out, there's no explicit way to wait for a given time at all. What you can, and should, do, is set up a callback to be executed after a certain amount of time. The JavaScript API for this is a function called setTimeout(), and there's also a separate function called setInterval() that sets up the callback to be run every n milliseconds.

The APIs are string-based, that is, callback functions and arguments are passed as JavaScript text. That alone calls for some quite un-intentional coding, for example, check out the source code for this SVG file:

function start_animation()
{        
    setTimeout("anim_down('subject', 10)", 25);
}

function anim_down (id, n)
{
    var elem = document.getElementById(id);
    translate (elem, 0, 10);

    if (n)
        setTimeout("anim_down('" + id + "'," + (n - 1) + ")", 25);
    else
        setTimeout("anim_left('" + id + "', 20)", 25);
}

As you can see, it's a pain in the ass to make sure everything is nicely representable as a string – e. g., XML element ID's are passed around instead of pointers to the actual elements. But you can always store state in some global variable, that's not the real problem. To see what the problem is, try clicking repeatedly on the button and see how the animation gets deformed. It's not hard to guess what's happening: a second timeout-cascade is set up before the first one had time to finish.

So in the bubble() function defined above, all swapping animations would start simultaneously. And there's no way to select() or join() or whatever until a certain timeout has run. We need to create a timeout queue.

Look at the code below for a minimalistic approach. Timeouts that register timeouts themselves should set/clear current_timeout, like in this example.

var timeout_queue = [];
var timeout_queue_timeout = 0;
var current_timeout = null; 
var timeout_queue_freq = 100;

function pop_timeout ()
{
    if (!timeout_queue.length)
    {
        clearInterval(timeout_queue_timeout);
        timeout_queue_timeout = 0;
        return;
    }
    
    if (!current_timeout)
    {
        var timeout = timeout_queue.shift();
        timeout.run ();
    }
}

function queue_timeout(timeout)
{
    timeout_queue.push(timeout);
    if (!timeout_queue_timeout)
        timeout_queue_timeout = setInterval("pop_timeout()", timeout_queue_freq);

    pop_timeout ();
}

Try clicking multiple times in quick succession in the second example: you won't see any corruption in the animation this time. Also, if you look at the code, thanks to the pseudo-synchronization, the animation can be expressed more intentionally:

function start_animation()
{
    animate('subject', 0, 10, 10);
    animate('subject', 10, 0, 20);
    animate('subject', 0, -10, 10);
}

« RSS feeds to ease transition from Advogato 
All entries
 Tombol a kompetencia »