m_poorUser m_poorUser - 7 months ago 11
Javascript Question

$.when.apply returns results with different structure according to length of input array

I am trying to write a generic code to get results of promises. See the sample code.

https://jsfiddle.net/f995r4ue/

var testUri = 'https://fiddle.jshell.net/robots.txt';

var promises = [];
promises.push($.get(testUri));
$.when.apply($, promises).then(function() {
// arguments is array of objects
console.log(arguments);
});

var promises = [];
promises.push($.get(testUri));
promises.push($.get(testUri));
$.when.apply($, promises).then(function() {
// arguments is array of arrays
console.log(arguments);
});


As explained in the code, if input array has only one promise, result structure is not same with other cases.

Am I doing something wrong?

Answer

This is a by-product of the crazy way that jQuery Ajax promises work and how jQuery decided to implement $.when(). It is somewhat odd behavior that is occasionally useful, but inconsistent enough to regularly drive you nuts. The inconsistency of behavior is not a good fundamental design and I believe jQuery plans to clean this up in jQuery 3.0 when they are supposed to be compatible with the ES6 standard promise specification (which does not behave this odd way).

Here's what is going on. A jQuery Ajax promise resolves with three values (completely non-standard for a promise). If you only pass one of those promises to $.when(), then $.when() passes that promise through directly (per the doc, it doesn't even create a new promise). As such, when that Ajax promise resolves and calls it's .then() handler, it will call the .then() handler the way the original promise would have which is with three separate arguments. If you look at the arguments object for the .then() handler you attached to $.when() in this case, you will see three discrete arguments.

But, if you pass more than one promise to $.when(), then it creates a new superset promise that monitors all the promises you passed. If each underlying promise resolved with more than one value, then that superset promise will put the arguments from each individual promise into its own array and it will pass to the .then() handler each individual array as a separate argument. If you look at the arguments object, you will see an array of arrays. If each underlying promise only resolved with a single value, then each argument would simply be that value - another point of inconsistency.

I am looking forward to jQuery 3.0 that says it is going to follow the promise specification and it will stop doing this odd sort of stuff.

These are examples from the jQuery doc:

// single promise passed to $.when()
// arguments to `$.when()` are identical to what that single promise would
//    have sent to its .then() handler which for a jQuery Ajax
//    function is three separate arguments
$.when( $.ajax( "test.aspx" ) ).then(function( data, textStatus, jqXHR ) {
  alert( jqXHR.status ); // Alerts 200
});

// multiple promises passed to $.when()
// Each argument to .then() is an array of arguments
$.when( $.ajax( "/page1.php" ), $.ajax( "/page2.php" ) ).then(function( a1, a2 ) {
  // a1 and a2 are arguments resolved for the page1 and page2 ajax requests, respectively.
  // Each argument is an array with the following structure: [ data, statusText, jqXHR ]
  var data = a1[ 0 ] + a2[ 0 ]; // a1[ 0 ] = "Whip", a2[ 0 ] = " It"
  if ( /Whip It/.test( data ) ) {
    alert( "We got what we came for!" );
  }
});

So, yes the behavior is consistent. If you don't know how many arguments to expect, you will have to test the type of what you get to decide how to process it.

So, the non-standard and inconsistent things happening here are:

  1. jQuery Ajax calls resolve with three separate arguments (promise standard says you get one argument only and if you have more than one value to return, you either return an array or an object)
  2. $.when() behaves differently if passed one promise or multiple promises.
  3. $.when() behaves differently if the underlying promises passed to it resolve with a single value or multiple values.

The ES6 promise world is much simpler because a promise never resolves with multiple values. If the programmer wants to communicate back multiple values, then you resolve with an array or an object (still a single value as far as the promise is concerned) and none of this inconsistent shenanigans that jQuery is doing has to be done. You always know exactly what you're going to get in Promise.all() - you always get an array of results.