JavaScript task chaining

From Trephine

Jump to: navigation, search
« JavaScript string building benchmarks Site improvements - fighting with Disqus »

[subscribe] Recent blog entries

Live Demos

JavaScript task chaining

Sometimes, when performing a long operation in JavaScript, it's beneficial to break up the job, inserting timeouts between tasks. Doing so gives the browser a chance to execute other pending timeout-bound operations and repaint the page, applying any DOM changes. This article discusses one method for implementing these breaks called "task chaining".

Examples of when you might want to do this include benchmark tests, where the capabilities of the browser are being specifically stretched, or DHTML effects such as sliding an element from one position to another. As with any programming problem, there are many ways to achieve the intended effect. The benefit of task chaining is its simplicity.

First, we start with an array of functions (tasks) to execute. Constructing a tasks array might look something like this:

var tasks = [];
tasks.push( function() { /* do something */ } );
tasks.push( function() { /* do something else */ } );

The goal is to call each task in order, with a given timeout in between each call. In this article, the timeout will always be 100 milliseconds, but the best value for any given use-case will vary.

At first glance, you might think that we could get away with the following:

for (var i=0; i<tasks.length; i++) {
  setTimeout( tasks[i], 100 );
}

Unfortunately, this does not do what we want. The above snippet will immediately queue up all the tasks to execute 100 milliseconds in the future, at which time each task will be executed in immediate succession.

That is, with no time in between calls. To resolve this, it's tempting to just add to the timeout on each iteration of the loop:

for (var i=0; i<tasks.length; i++) {
  setTimeout( tasks[i], 100 * i );
}

This is better, but still doomed to fail over time. The problem here is that the implementation assumes that tasks take no time at all to complete. For example, the eleventh task is scheduled to begin in 100 * 10 = 1000 milliseconds (1 second), regardless of how long the first ten tasks take to complete.

The longer the list of tasks, the worse the skew will be at the end. The skew will manifest itself as apparent acceleration since the time between tasks will decrease as the timeouts begin to stack up.

What we need is to set the timeout following the successful completion of each task:

var pos = 0, chain = function() {
  tasks[pos++]();
  if (pos<tasks.length) setTimeout( chain, 100 );
};
chain();

The above snippet defines and immediately executes a chain function which pulls the next task, executes it, then sets a timeout to continue if there are any tasks left.

We can avoid defining the "chain" variable by using an anonymous function instead:

var pos = 0;
(function(){
  tasks[pos++]();
  if (pos<tasks.length) setTimeout( arguments.callee, 100 );
})();

The above snippet is closer to ideal, but suffers from the problem that the first task will be executed immediately. That is, during the same JavaScript execution path. To solve this, we can wrap the outer anonymous function in a setTimeout() call itself:

var pos = 0;
setTimeout( function() {
  tasks[pos++]();
  if (pos<tasks.length) setTimeout( arguments.callee, 100 );
}, 100 );

Now that we have a fairly solid implementation, it makes sense to encapsulate it within a function so that we don't have to write all that out each time.

function chain( tasks, delay ) {
  if (!tasks || !tasks.length) return;
  var pos = 0, delay = delay || 100;
  setTimeout( function() {
    tasks[pos++]();
    if (pos<tasks.length) setTimeout( arguments.callee, delay );
  }, delay );
};

Which we call in this manner:

var tasks = [];
// ... add functions to tasks list
chain( tasks );

Or we can make chaining a natural ability of arrays by defining it within the Array prototype:

Array.prototype.chain = function chain( delay ) {
  var tasks = this, pos = 0, delay = delay || 100;
  setTimeout( function() {
    tasks[pos++]();
    if (pos<tasks.length) setTimeout( arguments.callee, delay );
  }, delay );
  return this;
};

And then make calls such as this:

var tasks = [];
// ... add functions to tasks list
tasks.chain();

That's all there is to it. If this article helped you think about chaining JavaScript timeouts, or if you have any questions about anything above, please leave a comment. Thanks!

Public domain declaration

Just so there's no confusion: all of the code snippets on this page are provided "AS IS", without warranty of any kind, express or implied.

All of the code snippets on this page are hereby released into the public domain by the me, the copyright holder. This applies worldwide. Or in case this is not legally possible: The copyright holder grants any entity the right to use this work for any purpose, without any conditions, unless such conditions are required by law.

If you'd feel better with a "real" license, you're free to use code snippets on this page under the MIT license as described on the about page.

Any links back to this site are always appreciated, but not required. Enjoy!

--Jim R. Wilson (jimbojw) 11:35, 4 May 2009 (UTC)
Personal tools