12

I have a simple array of urls, and I want to load each one with jQuery. I was using $.get, but I cannot seem to get it to work with $.Deferred, so I switched to $.ajax - I almost have it working, but the results I am getting are .. odd. I was hoping someone could help me make this work better.

var results = [], files = [
   'url1', 'url2', 'url3'
];

$.when(
   $.ajax(files[0]).done(function(data) { 
      results.push(data); console.log("step 1.0"); 
   }),
   $.ajax(files[1]).done(function(data) { 
      results.push(data); console.log("step 1.1"); 
   }),
   $.ajax(files[2]).done(function(data) {
      results.push(data); console.log("step 1.2"); 
   })
).then(function(){
   console.log("step 2");
});

This should output..

  • step 1.0
  • step 1.1
  • step 1.2
  • step 2

And then the results array contains the result of all 3 ajax requests. Is this possible?

jfriend00
  • 683,504
  • 96
  • 985
  • 979
Ciel
  • 4,290
  • 8
  • 51
  • 110
  • 1
    What is the problem you're having with this? The requests may complete in a different order, so you may end up with `step 1.2, step 1.0, step 1.1, step 2`, but the array will always be filled before the `then()` is executed. – Rory McCrossan Jul 11 '14 at 19:59
  • I absolutely need them to complete in the correct order. It is important to the code working correctly. – Ciel Jul 11 '14 at 20:00
  • I'm getting different results. I'm getting `step 2` firing before any of the step 1s. – Ciel Jul 11 '14 at 20:00
  • if you know the number of steps you can set value to proper array index instead of just blindly using `push` – charlietfl Jul 11 '14 at 20:01
  • `I need them to complete in the correct order` in that case this pattern will not work for you. The requests will be fired in order, but it's down to the server which order it responds in. As I said above, this is non-deterministic. If they must be done in order, you need to chain your requests - that is, fire the next request in the callback of the previous one. This will be slow though. – Rory McCrossan Jul 11 '14 at 20:01
  • Yeah, I get that. But I have no idea how else to loop through an array and wait for it to finish. The actual order of the items in the array isn't as important as them all being done before the second step - and right now, they are not working like that. – Ciel Jul 11 '14 at 20:02
  • This library will do what you need to do very easily: https://github.com/caolan/async Check out the `series` or `eachSeries` methods. – Mike Jul 11 '14 at 20:08
  • I need to do this without another library, unfortunately. It is because I can't use another library that I am having to solve this. – Ciel Jul 11 '14 at 20:09
  • I will however give `async` a try - though I'm a bit hesitant on adding another library on top of the dozens I already have just for a single use. – Ciel Jul 11 '14 at 20:18

2 Answers2

11

First off, you have to decide if you want your three ajax calls to be processed in parallel (running all at the same time, with less overall running time) or in sequence where one ajax calls runs, completes and then you launch the next ajax call. This is a key design decision that impacts how you do this.

When you use $.when() you are launching all three ajax calls in parallel. If you examine the results only when all have completed, you can still process the results in a specific order (since you will be processing them only when all results are available and they will be available in the order requested). But, when doing it this way all the ajax calls will be initially sent at once. This will give you a better end-to-end time so if this is feasible for the types of requests, this is generally a better way to do it.

To do that, you can restructure what you have to something like this:

Run in Parallel

var files = [
   'url1', 'url2', 'url3'
];

$.when($.ajax(files[0]),$.ajax(files[1]),$.ajax(files[2])).done(function(a1, a2, a3) {
   var results = [];
   results.push(a1[0]);
   results.push(a2[0]);
   results.push(a3[0]);
   console.log("got all results")
});

Because you're waiting until the .done() handler for $.when() has been called, all the ajax results are ready at once and they are presented by $.when() in the order they were requested (regardless of which one actually finished first), so you get the results as quick as possible and they are presented in a predictable order.

Note, I also moved the definition of the results array into the $.when() done handler because that's the only place you know the data is actually valid (for timing reasons).


Run in Parallel - Iterate Arbitrary Length Array

If you had a longer array, you might find it better to iterate through your array with something like .map() to process them all in a loop rather than listing them individually:

var files = [
   'url1', 'url2', 'url3', 'url4', 'url5', 'url6', 'url7'
];

$.when.apply($, files.map(function(url) {
    return $.ajax(url);
})).done(function() {
    var results = [];
    // there will be one argument passed to this callback for each ajax call
    // each argument is of this form [data, statusText, jqXHR]
    for (var i = 0; i < arguments.length; i++) {
        results.push(arguments[i][0]);
    }
    // all data is now in the results array in order
});

Sequence the Ajax Calls

If, on the other hand, you actually want to sequence your ajax calls so the 2nd one doesn't start until the first one finishes (something that may be required if the 2nd ajax call needs results from the 1st ajax call in order to know what to request or do), then you need a completely different design pattern and $.when() is not the way to go at all (it only does parallel requests). In that case, you probably just want to chain your results with x.then().then() and you can then output the log statements in the sequence you asked for like this.

  $.ajax(files[0]).then(function(data0) {
      console.log("step 1.0");
      return $.ajax(files[1]);
  }).then(function(data1) {
      console.log("step 1.1");
      return $.ajax(files[2]);
  }).done(function(data2) {
      console.log("step 1.2");
      // all the ajax calls are done here
      console.log("step 2");
  });

Console Output:

step 1.0
step 1.1
step 1.2
step 2

This structure can also be put into a loop to automatically run it for N sequential ajax calls if your array of files is longer. While you could collect the results as you go into the results array, often the reason things are done sequentially is that the prior results are consumed by the next ajax call so you often only need the final result. If you wanted to collect the results as you go, you could certainly push them into the results array at each step.

Notice, the advantages that promises offer here in that you can sequence operations while staying at the same top level of nesting and not getting further and further nested.


Sequence the Ajax Calls - Iterate Arbitrary Length Array

Here's what the sequencing would look like in a loop:

var files = [
   'url1', 'url2', 'url3', 'url4', 'url5', 'url6', 'url7'
];

var results = [];
files.reduce(function(prev, cur, index) {
    return prev.then(function(data) {
        return $.ajax(cur).then(function(data) {
            console.log("step 1." + index);
            results.push(data);
        });
    })
}, $().promise()).done(function() {
    // last ajax call done
    // all results are in the results array
    console.log("step 2.0");
});

Console Output:

step 1.0
step 1.1
step 1.2
step 1.3
step 1.4
step 1.5
step 1.6
step 2

The Array.prototype.reduce() method works handily here because it accumulates a single value as you process each individual array element which is what you need to do as you add .then() for each array element. The .reduce() iteration is started with an empty/resolved promise with $().promise() (there are other ways to also create such a promise) which just gives us something to start doing .then() on that is already resolved.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • This is extremely helpful. I am starting to see the wisdom in using an external library, even with the possible overhead it may incur. Right now I only need to do this in one place, but if I need to do it two or more times, the amount of extra work to make it function becomes more damaging. – Ciel Jul 12 '14 at 15:32
  • @Ciel - I don't understand your comment. Why would you need an external library? You can make any of these options into a reusable function with only a few more lines of code. – jfriend00 Jul 12 '14 at 17:11
  • Sorry. I've been a bit distracted, and only today got the chance to really try this out. Thank you so much for all of your help, I learned a lot from this example alone. – Ciel Jul 13 '14 at 22:40
  • Hi @jfriend00, the second code snippet got some problems. If there are only one url in the array, the return arguments array will become [Object, "success", Object] instead of [[Object, "success", Object]]. So in this case, we shall only take the first one instead of take all of them. Btw, I am another Ciel. Haha. – Ciel Apr 10 '15 at 07:10
  • @Ciel - yes, that is an annoyance of `$.when()` as it behaves differently if only passed a single item. – jfriend00 Apr 10 '15 at 15:30
3

You should access the return values from .then instead of each .done. Additionally, .map is your friend.

var results = [], files = [
   'url1', 'url2', 'url3'
];

$.when.apply($, $.map(files, function (file) {
    return $.ajax(file);
})).then(function (dataArr) {
    /* 
     * dataArr is an array of arrays, 
     * each array contains the arguments 
     * returned to each success callback
     */
    results = $.map(dataArr, function (data) {
        return data[0]; // the first argument to the success callback is the data
    });
    console.log(results);
});

the arguments passed to .then will be in the same order that they were passed to .when

Kevin B
  • 94,570
  • 16
  • 163
  • 180
  • 1
    I don't think you're handling the data from `$.when()` correctly. It doesn't give you a single argument that is an array of arrays. It gives you a series of arguments where each argument is an array of three values. See the second to last code example here http://api.jquery.com/jquery.when/ on line 3. – jfriend00 Jul 11 '14 at 21:38