113

I'm reading about Deferreds and Promises and keep coming across $.when.apply($, someArray). I'm a little unclear on what this does exactly, looking for an explanation that one line works exactly (not the entire code snippet). Here's some context:

var data = [1,2,3,4]; // the ids coming back from serviceA
var processItemsDeferred = [];

for(var i = 0; i < data.length; i++){
  processItemsDeferred.push(processItem(data[i]));
}

$.when.apply($, processItemsDeferred).then(everythingDone); 

function processItem(data) {
  var dfd = $.Deferred();
  console.log('called processItem');

  //in the real world, this would probably make an AJAX call.
  setTimeout(function() { dfd.resolve() }, 2000);    

  return dfd.promise();
}

function everythingDone(){
  console.log('processed all items');
}
Stobbej
  • 1,010
  • 9
  • 16
manafire
  • 5,776
  • 3
  • 37
  • 50
  • 1
    `.done()` can be used in place of `.then` in this case, just FYI – Kevin B Feb 08 '13 at 16:39
  • 2
    fwiw, there's a Deferred port to underscore that allows passing a single array to `_.when` so you don't need to use `apply` – Eevee Feb 08 '13 at 16:45
  • Learn more about `.apply`: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/apply. – Felix Kling Feb 08 '13 at 16:48
  • *related*: http://stackoverflow.com/questions/1986896/what-is-the-difference-between-call-and-apply – Felix Kling Feb 08 '13 at 16:50
  • 1
    The article the OP refers to in his first sentence has moved locations - it's now at : http://flaviocopes.com/blog/deferreds-and-promises-in-javascript . – glaucon Oct 27 '15 at 03:09

7 Answers7

164

.apply is used to call a function with an array of arguments. It takes each element in the array, and uses each as a parameter to the function. .apply can also change the context (this) inside a function.

So, let's take $.when. It's used to say "when all these promises are resolved... do something". It takes an infinite (variable) number of parameters.

In your case, you have an array of promises; you don't know how many parameters you're passing to $.when. Passing the array itself to $.when wouldn't work, because it expects its parameters to be promises, not an array.

That's where .apply comes in. It takes the array, and calls $.when with each element as a parameter (and makes sure the this is set to jQuery/$), so then it all works :-)

Rocket Hazmat
  • 204,503
  • 39
  • 283
  • 323
  • 3
    when multiple promises are passed to $.when method. In what order they will execute ? one after the other or in parallel ? – Darshan Nov 25 '13 at 17:23
  • 22
    @Darshan: You don't "run" promises. You wait for them to be resolved. They are executed when created, `$.when` just waits for all of them to be finished before continuing. – Rocket Hazmat Dec 02 '13 at 15:27
  • 1
    what about the difference between `$.when($, arrayOfPromises).done(...)` and `$.when(null, arrayOfPromises).done(...)` (which I found both as proposed solutions in the forums...) – zeroquaranta Jun 21 '17 at 08:34
63

$.when takes any number of parameters and resolves when all of these have resolved.

anyFunction.apply(thisValue, arrayParameters) calls the function anyFunction setting its context (thisValue will be the this within that function call) and passes all the objects in arrayParameters as individual parameters.

For example:

$.when.apply($, [def1, def2])

Is the same as:

$.when(def1, def2)

But the apply way of calling allows you to pass an array of unknown number of parameters. (In your code, you are saying that you data comes from a service, then that is the only way to call $.when)

IsmailS
  • 10,338
  • 18
  • 73
  • 128
Pablo
  • 1,023
  • 10
  • 9
16

Here, the code fully documented.

// 1. Declare an array of 4 elements
var data = [1,2,3,4]; // the ids coming back from serviceA
// 2. Declare an array of Deferred objects
var processItemsDeferred = [];

// 3. For each element of data, create a Deferred push push it to the array
for(var i = 0; i < data.length; i++){
  processItemsDeferred.push(processItem(data[i]));
}

// 4. WHEN ALL Deferred objects in the array are resolved THEN call the function
//    Note : same as $.when(processItemsDeferred[0], processItemsDeferred[1], ...).then(everythingDone);
$.when.apply($, processItemsDeferred).then(everythingDone); 

// 3.1. Function called by the loop to create a Deferred object (data is numeric)
function processItem(data) {
  // 3.1.1. Create the Deferred object and output some debug
  var dfd = $.Deferred();
  console.log('called processItem');

  // 3.1.2. After some timeout, resolve the current Deferred
  //in the real world, this would probably make an AJAX call.
  setTimeout(function() { dfd.resolve() }, 2000);    

  // 3.1.3. Return that Deferred (to be inserted into the array)
  return dfd.promise();
}

// 4.1. Function called when all deferred are resolved
function everythingDone(){
  // 4.1.1. Do some debug trace
  console.log('processed all items');
}
Yanick Rochon
  • 45,203
  • 21
  • 112
  • 182
  • 7
    `$.when.apply($, array)` is *not* the same as `$.when(array)`. It's the same as: `$.when(array[0], array[1], ...)` – Rocket Hazmat Feb 08 '13 at 16:38
  • 1
    That's the main reason why is used with _.apply_, you don't know hoy many elements processItemsDeferred has – Pablo Feb 08 '13 at 16:42
2

Unfortunately I can not agree with you guys.

$.when.apply($, processItemsDeferred).always(everythingDone);

Will call everythingDone as soon as one deferred gets rejected, even if there are other deferreds that are pending.

Heres the full script (I recommend http://jsfiddle.net/):

var data = [1,2,3,4]; // the ids coming back from serviceA
var processItemsDeferred = [];

for(var i = 0; i < data.length; i++){
  processItemsDeferred.push(processItem(data[i]));
}

processItemsDeferred.push($.Deferred().reject());
//processItemsDeferred.push($.Deferred().resolve());

$.when.apply($, processItemsDeferred).always(everythingDone); 

function processItem(data) {
  var dfd = $.Deferred();
  console.log('called processItem');

  //in the real world, this would probably make an AJAX call.
  setTimeout(function() { dfd.resolve(); }, 2000);    

  return dfd.promise();
}

function everythingDone(){
  alert('processed all items');
}

It this a bug? I would like to use this like the gentleman above described it.

  • 1
    The first reject will fire the always, but not the .then. See my http://jsfiddle.net/logankd/s5dacgb3/ that I made from your example. I'm using JQuery 2.1.0 in this example. – AlignedDev Jan 30 '15 at 19:30
  • 1
    This is as intended. There are a ton of cases where you'd want to know as soon as *something* fails, not wait for everything to complete and check if there were failures. Especially if the processing cannot continue after any failure, why wait for the rest to finish / fail? As the other comment suggested, you can use .then or .fail & .done pair. – MPavlak Apr 27 '16 at 17:31
  • @GoneCoding It is not useful. The OP asked for what the apply() does and you suggested a terrible alternative that should never be used :) that IS what the down vote button is for. I also did not use it until you were refusing to provide WHY you did it that why (more than your preference to avoid arrays for some reason) – MPavlak Apr 27 '16 at 19:28
  • @GoneCoding Thank you for taking down that answer – MPavlak Apr 27 '16 at 19:29
  • 1
    @GoneCoding lol, I read your solution and provided feedback. you did not supply an answer to the original question. You could not elaborate why it was as it was. It's people like you that provide terrible solutions to people that are learning. You clearly have limited javascript skill and are taking me to be the n00b. I indicated why it was wrong and you couldn't even read the code and instead tell me I'm wrong. good job buddy! – MPavlak Apr 27 '16 at 19:33
1

Maybe someone can find this useful:

$.when.apply($, processItemsDeferred).then(everythingDone).fail(noGood);

everythingDone isn't called in case of any reject

Krzysztof Raciniewski
  • 4,057
  • 2
  • 16
  • 39
0

$.when alone makes it possible for a callback to be called when every promises passed to it are resolved/rejected. Normally, $.when takes a variable number of arguments, using .apply makes it possible to pass it an array of arguments, it's very powerful. For more info on .apply: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/apply

Roger C
  • 338
  • 1
  • 5
0

Thanks for your elegant solution:

var promise;

for(var i = 0; i < data.length; i++){
  promise = $.when(promise, processItem(data[i]));
}

promise.then(everythingDone);

Just one point: When using resolveWith to get some parameters, it breaks because of the initial promise set to undefined. What i did to make it work:

// Start with an empty resolved promise - undefined does the same thing!
var promise;

for(var i = 0; i < data.length; i++){
  if(i==0) promise = processItem(data[i]);
  else promise = $.when(promise, processItem(data[i]));
}

promise.then(everythingDone);
Paul Roub
  • 35,100
  • 27
  • 72
  • 83
user3544352
  • 49
  • 1
  • 7
  • 2
    While that does work, it is not really elegant. you are creating promises that represent the up-to-ith deferred being done so that the last iteration contains "when(workToDo[0..i-1], workToDo[i])" or more plainly "when all previous work and this work is done". This means that you have i + 1 when wrappers over your promises. Also, when doing this kinda of stuff, simply unwrap the first iteration. var promise = processItem(data[0]); for(var i = 1; i < data.length; i++){ promise = $.when(promise, processItem(data[i])); } – MPavlak Apr 27 '16 at 17:27