Pass in an Array of Deferreds to $.When()

Pass in an array of Deferreds to $.when()

To pass an array of values to any function that normally expects them to be separate parameters, use Function.prototype.apply, so in this case you need:

$.when.apply($, my_array).then( ___ );

See http://jsfiddle.net/YNGcm/21/

In ES6, you can use the ... spread operator instead:

$.when(...my_array).then( ___ );

In either case, since it's unlikely that you'll known in advance how many formal parameters the .then handler will require, that handler would need to process the arguments array in order to retrieve the result of each promise.

How to pass to when array of deferred with their own callbacks

Since this is not any type of behavior that is built into promises, you could implement your own. jQuery will call .done() when that particular promise is done (not when the group promise is done) so you will have to specifiy the callback a different way.

One scheme would be for you to add a .cb property to each promise and when the whole group is done that callback will get called. You can then make a superset of $.when that will look for that callback and call it if it exists when the whole group is done:

$.whenAfter = function(promiseArray){
return $.when.apply($, promiseArray).then(function() {
var results = Array.prototype.slice.call(arguments);
promiseArray.forEach(function(p, index) {
if (p.cb) {
p.cb(results[index]);
}
});
return results;
});
}

Or, if you don't want to add the property to the promise, you could pass a separate callback array that corresponds to the promiseArray:

$.whenAfter = function(promiseArray, callbackArray){
return $.when.apply($, promiseArray).then(function() {
var results = Array.prototype.slice.call(arguments);
promiseArray.forEach(function(p, index) {
var cb = callbackArray[index];
if (cb) {
cb(results[index]);
}
});
return results;
});
}

If you have callbacks that must remain internal to your functions that you don't want to execute until some other set of operations are done, then I'd suggest you pass in a promise and you execute those callbacks when that promise is resolved.

function SomeFunction1(p1){
var p2 = $.ajax(...);
// now wait for both our async operation and some other async operation
// to be done before carrying out the rest of our business
$.when(p1, p2).then(function(a1, a2) {
// now everything else is done too so we can carry out the rest of our business
});

// return p2 so other things can know when this ajax operation is done
return p2;
}

And, you could combine multiple of these like this:

var def = $.Deferred();
var p = def.promise();

$.when(SomeFunction1(p), SomeFunction2(p), SomeFunction3(p)).then(def.resolve, ref.reject);

My experience with promises, tells me this is ugly code, but I'm not sure at the moment how to make this particular type of solution work cleaner.


Personally, I think I'd just make SomeFunctionX return both a promise and a callback so the callback can be called from the outside where we actually know things are done:

function SomeFunction1(){
var p = $.ajax(...);
function callback() {
// do something here after we're done and others are done too
}
return {promise: p, callback: callback}
}

Then, at the point you want to call several functions, you put them in an array and iterate over that array, collecting results and calling callbacks when appropriate:

  var funcs = [SomeFunction1, SomeFunction2, SomeFunction3];

var callbacks = [];
var promises = funcs.map(function(fn) {
var retVal = fn();
callbacks.push(retVal.callback);
return retVal.promise;
});
$.when.apply($, promises).then(function() {
var args = Array.prototype.slice.call(arguments);
callbacks.forEach(function(cb, index) {
cb(args[index]);
})
});

And, you could make this into a reusable function that you would just pass an array of functions that returned the right data structure (promise and callback):

function runAll(funcs) {
var callbacks = [];
var promises = funcs.map(function (fn) {
var retVal = fn();
// if it only returns only a thenable (not our data structure), then just return the promise
// this allows you to mix in functions that just return a promise
if (typeof retVal.then === "function") {
// assume no callback
callbacks.push(null);
return retVal;
}
callbacks.push(retVal.callback);
return retVal.promise;
});
return $.when.apply($, promises).done(function () {
try {
var args = Array.prototype.slice.call(arguments);
callbacks.forEach(function (cb, index) {
if (cb) {
cb(args[index]);
}
});
} catch(e) {
// if any callback throws an exception, then reject
return $.Deferred().reject(e);
}
});
});

var funcs = [SomeFunction1, SomeFunction2, SomeFunction3];

runAll(funcs).done(function(results) {
// all done here
}).fail(function(err) {
// some sort of error here
});

P.S. If you're using async operations that already create and return a promise such as $.get(), then you should not be creating your own deferred. That is a promise anti-pattern. You should just be returning the promise that has already been created.

How do you work with an array of jQuery Deferreds?

You're looking for

$.when.apply($, promises).then(function(schemas) {
console.log("DONE", this, schemas);
}, function(e) {
console.log("My ajax failed");
});

This will also work (for some value of work, it won't fix broken ajax):

$.when.apply($, promises).done(function() { ... }).fail(function() { ... });` 

You'll want to pass $ instead of null so that this inside $.when refers to jQuery. It shouldn't matter to the source but it's better then passing null.

Mocked out all your $.ajax by replacing them with $.when and the sample works

So it's either a problem in your ajax request or the array your passing to fetch_schemas.

How to make an array of Deferred objects

Thanks to all for great advice.

I used a combination of the suggested techniques in my solution.

The key thing was to make an array of promises, and push onto it the required calls (each with its own array chunk passed as a parameter) to the function that makes the ajax request. One thing I hadn't previously realised is that this calls the ajaxCall() function at that very moment, and that's ok because it returns a promise that is pushed onto the array.

After this, the 'when.apply' line does the trick in waiting until all the ajax promises are fulfilled. The arguments of the 'then' function are used to collate all the results required (obviously, the exact mechanism for that depends on the format of your returned arguments). The results are then sent to theResultsHandler(), which takes the place of the original callback in the code I first posted in my question.

Hope this is useful to other Promise-novices!

The ajax-calling function is:

ajaxCall: function(d) {
return $.ajax('/myURL', {
contentType: 'application/json',
data: d,
dataType: 'json',
type: 'POST'
});
},

And inside the flush() function...

    var promises = [];
var i, j;

for (i=0; i<batchChunks.length; i++)
{
promises.push(self.ajaxCall(batchChunks[i]));
}

var results = [];

return $.when.apply($, promises).then(function(){

console.log("arguments = " + JSON.stringify(arguments));

for (i = 0; i < arguments.length; i++)
{
for (j = 0; j < arguments[i][0].length; j++)
{
results.push(arguments[i][0][j]);
}
}

return self.theResultsHandler(results);
});

$.when() array of deferreds

You're calling loadPostGamesLoadData, not referencing it

$.when.apply($, deferredLoads).done(loadPostGamesLoadData);

Otherwise it looks fine, assuming loadGames and loadPlayoffs are functions that resolve or reject the Deferred.

JQuery - $.when syntax for array of Deferred objects

Yes, I have stumbled upon this as well: when does not easily allow to be passed an array. But you could use apply to achieve the desired result.

$.when.apply($, getCustomerDataCalls(customerIds))

.When() and .done() using an array with .done

If you look at the examples for $.when, you see that the call back gets passed an argument for each promise. If that promise came from an Ajax call, then each argument is an array of the form [ data, statusText, jqXHR ].

So you you just have iterate over the arguments and extract the first element. $.map makes that very easy:

$.when.apply($, requests)
.then(function() {
return $.map(arguments, function(v) {
return v[0];
});
})
.done(callback);

passing an array of jquery get requests into $.when()

You need:

$.when.apply($, myArray);

i.e. using Function.apply to call $.when() with this === $ and the rest of the arguments being the contents of the array myArray, where that array contains the deferred objects returned by each call to $.get.


To answer the second question from your comments, you could pass your array of deferred objects through this map function, which returns a new array of deferred objects which will be resolved at completion of each AJAX call, regardless of whether it succeeded or not.

var alwaysDef = def.map(function(old) {
var def = $.Deferred();
old.always(def.resolve);
return def;
});

Note that I'm not sure what parameters (if any) will end up being passed to the final .done function in this instance.

If that matters, the deferred objects are always called in order, so you can register a .done handler for each of the original promises that stores the AJAX results in an array for later use, and a .fail handler that stores a null value instead.

That will ensure that your final .when handler isn't invoked until after each of the individual results are available to you.

$.when.apply on array of deferreds runs before all resolved

Here's a general idea. I had several goals here.

  1. Get rid of the global variable that you were accumulating promises into.
  2. Make all nested promises be chained onto their parent promises so that you can just wait on the parent promises and everything will just work.
  3. To chain promises, I replace the completion callback in $.getJSON() with a .then() handler and then return the embedded promises from the .then() handler. This automatically chains them onto the parent so the parent won't itself get resolved until the embedded ones are also resolved. And, this works to an arbitrary depth of embedded promises.
  4. Get rid of the anti-pattern of creating and resolving new deffereds when promises already exist that can just be used.

In the process of getting rid of the globals, I had to remove the creation of promises from the course constructor because it can't return the promises. So, I created a course.init() where the promises are created and returned. Then, we can accumulate promises in a local variable and avoid the global.

So, here's the general idea:

var course = function( options ) {
...
var self = this;

this.p = { //prereqs object
a: [], // array for prereqs
rgx: /([A-Z]\w+[\ ])\w+/g,
parse: function() {
var promises = [];
// check if we have any prereqs to process
if (self.prereqs !== '') {
self.prereqs.replace(this.rgx,function(m) {
//search through prereq string (self.prereqs) and run ajax for each match
var id = m.split(' ');
promises.push($.getJSON('/ajax/ajaxPrereqs.php',{subj:id[0],crs:id[1]}).then(function(d) {
var d = d[0];
var c = new course({
... //create new course in self.p.a[]
}));
self.p.a.push(c);
// chain all the new promises created by c.init() onto our master promise
// by returning a new promise from the .then() handler
return(c.init());
}));
});
}
// return a master promise that is resolve when all the sub promises
// created here are all done
return $.when.apply($, promises);
}
};

// call this to run the initial parse
// returns a single promise that is resolve when all the promises are done
this.init = function() {
return this.p.parse();
};

...
return this;
}

$.getJSON('/ajax/ajaxPrereqs.php',{subj:$('#subj').val()}, function(data) {

var promises = [];
var COURSES = [];
$.each(data,function() {
var d = this;
var c = new course({
s: d.subj,
c: d.crs,
t: d.titleshrt,
crd: d.credits,
chr: d.contacthrs,
prereqs: d.prereqs
});
COURSES.push(c);
promises.push(c.init());
});
$.when.apply($, promises).then(function() {
console.log('promises all done');
console.log(promises.length);
$.each(promises,function() {console.log(this.state())})
$.each(COURSES,function() {
this.render();
})
});
});


Related Topics



Leave a reply



Submit