Mark Needham

Thoughts on Software Development

jQuery: Collecting the results from a collection of asynchronous requests

with 5 comments

Liz and I recently spent some time building a pair stair to show how long ago people had paired with each other and one of the things we had to do was make AJAX requests to get the pairing data for each person and then collate it all to build the stair.

Pair stair

The original attempt to do this looked a bit like this:

var people = ["Marc", "Liz", "Ken", "Duncan", "Uday", "Mark", "Charles"];
 
var grid = [];
$.each(people, function(index, person) {
  $.getJSON('/git/pairs/' + person, function(data) {
    // parse data and create somethingCool
    grid.push(somethingCool);
  });  
});
 
// do something with grid

When we try to do something with grid it is of course empty because we’ve attempted to access it before all of the callbacks (which populate it) have returned.

Pedro Teixeira has a nice blog post which explains how to solve this problem in node.js and we can use the same pattern here.

We need to write our own looping mechanism which is able to determine when the last callback has returned.

This is done by creating a copy of the people array and then manually iterating through it using shift.

var people = ["Marc", "Liz", "Ken", "Duncan", "Uday", "Mark", "Charles"];
var peopleCopy = people.slice(0), grid = [];
(function getPairs() {
  var person = peopleCopy.shift();
 
  if(peopleCopy.length == 0) {
    // do something with grid
  } else {
    $.getJSON("/git/pairs" + person, function(data) {
      // parse data and create somethingCool
      grid.push(somethingCool);
      getPairs();		
    })						
  }
})();

I tried to extract the asynchronous looping and ended up with the following function:

function asyncLoop(collection, seedResult, loopFn, completionFn) {
  var copy = collection.slice(0);
  (function loop() {
    var item = copy.shift();
 
    if(copy.length == 0) {
      completionFn(seedResult);
    } else {
      loopFn(item, seedResult, loop);
    }	
  })();	
}

Which could be called like this:

var people = ["Marc", "Liz", "Ken", "Duncan", "Uday", "Mark", "Charles"];
asyncLoop(people, [], function(name, grid, callBackFn) {
  // parse data and create something cool
  grid.push(somethingCool);
  callBackFn();
}, function(grid) {
  // do something with grid
});

I’m not sure that it reads that much clearer but it does push some of the boiler plate code away.

Be Sociable, Share!

Written by Mark Needham

September 25th, 2011 at 9:26 am

Posted in Javascript,jQuery

Tagged with ,

  • Artur

    Hmm… How about using deferreds? Seems to me like it will be much cleaner.

    $.when(ajaxDeferreds).then(function(results) { /* do something with the grid */ })

  • Oooh I didn’t know you could do that! Will give it a try, thanks for the tip

  • Hi Mark, I’ve written a blog post as a response with an alternative approach:

    http://www.lucagrulla.it/flow-control-in-javascript

  • Check out Brian Cavalier’s when.js.  It solves this problem gracefully as well as many other async/deferred use cases: https://github.com/briancavalier/when.js

  • Hey, thanks for the mention, John!  Yep, when.js is compatible with jQuery’s Deferred (and with Dojo, Q, promised-io, etc.), so you could do:

    when.all(ajaxDeferreds, function(results) { /* awesomeness */ }); 

    or similar to Artur’s example below:
    when.all(ajaxDeferreds).then(function(results) { /* do something with the grid */ });