Better way to handle a variable number of deferred requests in a JQuery $.when call?

1.2k views Asked by At

I'm writing an app that at one point needs to get the result some number of requests before proceeding. This seems like a likely candidate for using JQuery's when. eg:

 // requests is an array of Deferreds
 $.when.apply(null, requests).done(function(results) {
     // process results
 }

The catch is that the number of requests could be 1 or it could be more, and the when function handles these cases differently. With only 1 request, the callback gets the standard (data, textStatus, jqXHR) arguments, however when multiple requests are supplied the callback receives a sequence of arguments: an array of (data, textStatus, jqXHR) values for each of the requests. Frustratingly, JQuery seems to be treating a singleton array of requests the same as a single argument request, meaning that this case must be handled differently.

The best I could come up with to tease these cases apart feels kinda clunky and took me a while to figure out due to the subtleties of the resultant arguments in the different cases, and then there's the rookie gotcha of the request array needing to be wrapped up in a function so that it can be accessed in the closure.

$.when.apply(null, function (){return requests;}()).done(function(results) {
    if (requests.length == 1) {
        processResults(results);
    } else {
        for (var i=0; i < arguments.length; i++)
            processResults(arguments[i][0]);
    }
   moreStuff();
});

Is there a better or more elegant way than this?

2

There are 2 answers

0
jfriend00 On

I see this is an older question, but this issue came up for me recently and I came across this question while searching on the issue. I ended up created my own solution so I thought I'd share it as an answer here.

In a nutshell, $.when() is really not designed very well for use with dynamic numbers of arguments. In fact, it kind of sucks for dynamic arguments. It is both hard to use and inconsistent in its behavior.

It appears that the designers of the ES6 promises specification agree because the analog there Promise.all() is designed differently and solves both these problems. So, the best thing I could think of to deal with jQuery's $.when() issue is to make a new version of it that follows the Promise.all() rules and is thus simpler to use. Life is further complicated by the fact that jQuery Ajax promises return an array of three values which interacts with $.when() in the odd ways you mentioned.

So, there are two main changes to $.when() to make a $.all():

  1. Make it so that it accepts an array of promises, not separate arguments of promises. This keeps you from having to use .apply() just to send a variable number of promises to $.all().

  2. Make it so that the results form $.all() are always in array form, no matter how many promises were originally passed in. This is the inconsistency that you ask about in your question. Fortunately, it doesn't take a lot of code to fix this.

It can be done like this:

$.all = function (promises) {
    if (!Array.isArray(promises)) {
        throw new Error("$.all() must be passed an array of promises");
    }
    return $.when.apply($, promises).then(function () {
        // if single argument was expanded into multiple arguments, then put it back into an array
        // for consistency
        if (promises.length === 1 && arguments.length > 1) {
            // put arguments into an array
            return [Array.prototype.slice.call(arguments, 0)];
        } else {
            return Array.prototype.slice.call(arguments, 0);
        }
    })
};

You use it just like $.when() except that you always pass $.all() a single argument that is an array of promises (no need for .apply() any more) and it always resolves to a single argument that is an array of results (much easier to iterate dynamic numbers of promise results and it is always consistent).

So, if you had an array of ajax promises of arbitrary length:

var promises = [url1, url2, url3].map(function(url) {
    return $.get(url);
});


$.all(promises).then(function(results) {
    // results are always consistent here no matter how many promises
    // were originally passed in, thus solving your original problem
    for (var i = 0; i < results.length; i++) {
        console.log(results[i]);
    }
});
0
ergu On

I have a similar situation where I have a fixed number of unique deferred requests, but depending on the situation, I don't want to make all of those calls, and as such, I don't want to process all of the responses. My approach was to create a dummy deferred that returns an empty response.

// this immediately returns whatever 
// is passed in as response
function emptyAjaxResponse(response) {
    var deferred = $.Deferred().resolve(response);
    return deferred.promise();
}

function multipleAjaxCalls(someCondition) {

    // promise1 always needs to happen
    var promise1 = jQuery.getJSON("some url");

    // promise2 is always an array, but if someCondition is
    // not met, we won't bother to make the actual request,
    // just feed in an empty array.
    var promise2 = someCondition ? jQuery.getJSON("some other url") : emptyAjaxResponse([]);

    $.when(promise1, promise2).done(response1, response2) {
        // now, we can process response1 and response2
        // appropriately whether we made an actual ajax
        // call on promise2 or not.
    }
}