setTimeout Patterns in JavaScript

August 10th, 2010

Any halfway experienced JavaScripter is familiar with the setTimeout function. It takes two arguments: a function, and a number n. It will delay calling the function until n milliseconds (give or take a few) have passed. The delay is performed asynchronously. This means that the rest of your program will not halt while the timeout is ticking down.

SetTimeout provides a building block for many other useful patterns appearing in typical JavaScript development. In this article, I will document a few that I use frequently. I am interested in hearing what setTimeout patterns you have found useful in the comments.

async

function async (fn) {
    setTimeout(fn, 20);
}

Although, seemingly useless at a first glance, asynchronously delaying the execution of a block of code by only a very small amount of time is surprisingly useful. Suppose that you have the following code:

function getEmployeeInfo (name, callback) {
    var info = getCachedEmployeeInfo(name);
    if ( typeof info === "object" ) {
        callback(info);
    } else {
        $.get("/employees", { name: name }, function (info) {
            setCachedEmployeeInfo(name, info);
            callback(info);
        });
    }
}

Because of the way caching is implemented, the callback function is sometimes executed asynchornously, and sometimes it is not. This is a problem, and Oliver Steele has written about it more extensively than I will.

The solution is to create uniform behavior whether the information is cached or not, by forcing all branches of logic to be asynchronous.

function getEmployeeInfo (name, callback) {
    var info = getCachedEmployeeInfo(name);
    if ( typeof info === "object" ) {
        async(function () { callback(info); });
    } else {
        $.get("/employees", { name: name }, function (info) {
            setCachedEmployeeInfo(name, info);
            callback(info);
        });
    }
}

Asyc is also an excellent buidling block for further abstractions.

sometimeWhen

function sometimeWhen (test, then) {
    async(function () {
        if ( test() ) {
            then();
        } else {
            async(arguments.callee);
        }
    });
}

It is often the case that you want to execute a bit of code if some condition is met in the future. This is exactly the use case for sometimeWhen.

A representative example for sometimeWhen is testing for when some set of asynchronous operations have all completed. In the following example, getUrl performs a simple GET request, and getUrlsInBatch will take a list of URLs and a callback to call once all the URLs' request data has been received.

function getUrlsInBatch (urls, callback) {
    var results = [];

    for (var i = 0; i < urls.length; i++) {
        getUrl(urls[i], function (data) {
            results.push(data);
        });
    }

    // When the length of the results and urls are equal, that means every
    // request has completed and we can call the callback with all of the
    // requested data.
    sometimeWhen(function () { return results.length === urls.length; },
                 function () {
                     callback(results);
                 });
}

whenever

function whenever (test, then) {
    sometimeWhen(test, function () {
        then();
        sometimeWhen(test, arguments.callee);
    });
}

The whenever function is similar to sometimeWhen function in that it asynchronously polls a condition via the test function parameter. However, unlike sometimeWhen, it will continue testing for the condition forever, instead of stopping after the first time that test() returns truthy.

As is typically the case with the most interesting client side features, not all browsers and browser releases support the onhashchange event. Fear not, this doesn't present a significant threat to the savvy JavaScripter:

(function () {
    var hash = window.location.hash;
    whenever(function () { return window.location.hash !== hash; },
             function () {
                 hash = window.location.hash;
                 if ( typeof window.onhashchange === "function" ) {
                     window.onhashchange();
                 }
             });
}());

yieldingEach

During the processing and iterarion of large collections, the browser can become unresponsive. The processing and iteration code doesn't necessarily need to be faster, though that is still a possibility, but more often than not the code needs to yield control to the UI thread and let the browser become responsive again. This is typically performed with recursive calls to setTimeout in between iterations on the data set.

This pattern can be generalized in to the yieldingEach function. Items is the data set we will be iterating over. IterFn is a function that will be called on each item, similar to the function you might pass to Array.prototype.forEach, but if it returns false then the iteration will stop and the callback function will be called early. If iteration isn't halted prematurely, callback is called after all the items have been processed. It is passed the whole collection of items as it's only argument.

function yieldingEach (items, iterFn, callback) {
    var i = 0, len = items.length;
    async(function () {
        var result;

        // Process the items in batch for 50 ms, or while the result of
        // calling `iterFn` on the current item is not false..
        for ( var start = +new Date;
              i < len && result !== false && ((+new Date) - start < 50);
              i++ ) {
            result = iterFn.call(items[i], items[i], i);
        }

        // When the 50ms is up, let the UI thread update by defering the
        // rest of the iteration with `async`.
        if ( i < len && result !== false ) {
            async(arguments.callee);
        } else {
            callback(items);
        }
    });
}

yieldingMap

YieldingMap is a specialization of yieldingEach where you might want to perform the same transformation to each item in items and then do something else with the resulting array. It is a yielding version of the cross-browser-incompatible Array.prototype.map.

function yieldingMap (items, iterFn, callback) {
    var results = [];
    yieldingEach(items,
                 function (thing) {
                     results.push( iterFn.call(thing, thing) );
                 },
                 function () {
                     callback(results);
                 });
}

Conclusion

I know I have only touched on a corner of the large subject of the common patterns that arise and use cases for setTimeout. I am interested in hearing yours as well.

« Previous Entry

Next Entry »


Recent Entries

wu.js 2.0 on August 7th, 2014

Come work with me on Firefox Developer Tools on July 8th, 2014

Debugging Web Performance with Firefox DevTools - Velocity 2014 on June 26th, 2014

Beyond Source Maps on March 12th, 2014

Memory Tooling in Firefox Developer Tools in 2014 on March 4th, 2014

Hiding Implementation Details with ECMAScript 6 WeakMaps on January 13th, 2014

Re-evaluate Individual Functions in Firefox Developer Tools' Scratchpad on November 22nd, 2013

Testing Source Maps on October 2nd, 2013

Destructuring Assignment in ECMAScript 6 on August 15th, 2013

My Talk from Front Trends 2013 on June 21st, 2013

Creative Commons License

Fork me on GitHub