setTimeout Patterns in JavaScript
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.