Skip to content

Commit 356a3bc

Browse files
committed
Deferred: Separate the two paths in jQuery.when
Single- and no-argument calls act like Promise.resolve. Multi-argument calls act like Promise.all. Fixes gh-3029 Closes gh-3059
1 parent 0bd98b1 commit 356a3bc

File tree

6 files changed

+342
-226
lines changed

6 files changed

+342
-226
lines changed

build/tasks/promises_aplus_tests.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,20 @@ module.exports = function( grunt ) {
44

55
var spawnTest = require( "./lib/spawn_test.js" );
66

7-
grunt.registerTask( "promises_aplus_tests", function() {
7+
grunt.registerTask( "promises_aplus_tests",
8+
[ "promises_aplus_tests_deferred", "promises_aplus_tests_when" ] );
9+
10+
grunt.registerTask( "promises_aplus_tests_deferred", function() {
11+
spawnTest( this.async(),
12+
"./node_modules/.bin/promises-aplus-tests",
13+
"test/promises_aplus_adapter_deferred.js"
14+
);
15+
} );
16+
17+
grunt.registerTask( "promises_aplus_tests_when", function() {
818
spawnTest( this.async(),
919
"./node_modules/.bin/promises-aplus-tests",
10-
"test/promises_aplus_adapter.js"
20+
"test/promises_aplus_adapter_when.js"
1121
);
1222
} );
1323
};

src/deferred.js

Lines changed: 55 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,38 @@ function Thrower( ex ) {
1313
throw ex;
1414
}
1515

16+
function adoptValue( value, resolve, reject ) {
17+
var method;
18+
19+
try {
20+
21+
// Check for promise aspect first to privilege synchronous behavior
22+
if ( value && jQuery.isFunction( ( method = value.promise ) ) ) {
23+
method.call( value ).done( resolve ).fail( reject );
24+
25+
// Other thenables
26+
} else if ( value && jQuery.isFunction( ( method = value.then ) ) ) {
27+
method.call( value, resolve, reject );
28+
29+
// Other non-thenables
30+
} else {
31+
32+
// Support: Android 4.0 only
33+
// Strict mode functions invoked without .call/.apply get global-object context
34+
resolve.call( undefined, value );
35+
}
36+
37+
// For Promises/A+, convert exceptions into rejections
38+
// Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in
39+
// Deferred#then to conditionally suppress rejection.
40+
} catch ( /*jshint -W002 */ value ) {
41+
42+
// Support: Android 4.0 only
43+
// Strict mode functions invoked without .call/.apply get global-object context
44+
reject.call( undefined, value );
45+
}
46+
}
47+
1648
jQuery.extend( {
1749

1850
Deferred: function( func ) {
@@ -305,67 +337,45 @@ jQuery.extend( {
305337
},
306338

307339
// Deferred helper
308-
when: function() {
309-
var method, resolveContexts,
310-
i = 0,
311-
resolveValues = slice.call( arguments ),
312-
length = resolveValues.length,
340+
when: function( singleValue ) {
341+
var
342+
343+
// count of uncompleted subordinates
344+
remaining = arguments.length,
313345

314-
// the count of uncompleted subordinates
315-
remaining = length,
346+
// count of unprocessed arguments
347+
i = remaining,
348+
349+
// subordinate fulfillment data
350+
resolveContexts = Array( i ),
351+
resolveValues = slice.call( arguments ),
316352

317-
// the master Deferred.
353+
// the master Deferred
318354
master = jQuery.Deferred(),
319355

320-
// Update function for both resolving subordinates
356+
// subordinate callback factory
321357
updateFunc = function( i ) {
322358
return function( value ) {
323359
resolveContexts[ i ] = this;
324360
resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
325361
if ( !( --remaining ) ) {
326-
master.resolveWith(
327-
resolveContexts.length === 1 ? resolveContexts[ 0 ] : resolveContexts,
328-
resolveValues
329-
);
362+
master.resolveWith( resolveContexts, resolveValues );
330363
}
331364
};
332365
};
333366

334-
// Add listeners to promise-like subordinates; treat others as resolved
335-
if ( length > 0 ) {
336-
resolveContexts = new Array( length );
337-
for ( ; i < length; i++ ) {
338-
339-
// jQuery.Deferred - treated specially to get resolve-sync behavior
340-
if ( resolveValues[ i ] &&
341-
jQuery.isFunction( ( method = resolveValues[ i ].promise ) ) ) {
342-
343-
method.call( resolveValues[ i ] )
344-
.done( updateFunc( i ) )
345-
.fail( master.reject );
346-
347-
// Other thenables
348-
} else if ( resolveValues[ i ] &&
349-
jQuery.isFunction( ( method = resolveValues[ i ].then ) ) ) {
350-
351-
method.call(
352-
resolveValues[ i ],
353-
updateFunc( i ),
354-
master.reject
355-
);
356-
} else {
357-
358-
// Support: Android 4.0 only
359-
// Strict mode functions invoked without .call/.apply get global-object context
360-
updateFunc( i ).call( undefined, resolveValues[ i ] );
361-
}
362-
}
367+
// Single- and empty arguments are adopted like Promise.resolve
368+
if ( remaining <= 1 ) {
369+
adoptValue( singleValue, master.resolve, master.reject );
363370

364-
// If we're not waiting on anything, resolve the master
365-
} else {
366-
master.resolveWith();
371+
// Use .then() to unwrap secondary thenables (cf. gh-3000)
372+
return master.then();
367373
}
368374

375+
// Multiple arguments are aggregated like Promise.all array elements
376+
while ( i-- ) {
377+
adoptValue( resolveValues[ i ], updateFunc( i ), master.reject );
378+
}
369379
return master.promise();
370380
}
371381
} );

test/promises_aplus_adapter_when.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/* jshint node: true */
2+
3+
"use strict";
4+
5+
require( "jsdom" ).env( "", function( errors, window ) {
6+
if ( errors ) {
7+
console.error( errors );
8+
return;
9+
}
10+
11+
var jQuery = require( ".." )( window );
12+
13+
exports.deferred = function() {
14+
var adopted, promised,
15+
obj = {
16+
resolve: function() {
17+
if ( !adopted ) {
18+
adopted = jQuery.when.apply( jQuery, arguments );
19+
if ( promised ) {
20+
adopted.then( promised.resolve, promised.reject );
21+
}
22+
}
23+
return adopted;
24+
},
25+
reject: function( value ) {
26+
if ( !adopted ) {
27+
adopted = jQuery.when( jQuery.Deferred().reject( value ) );
28+
if ( promised ) {
29+
adopted.then( promised.resolve, promised.reject );
30+
}
31+
}
32+
return adopted;
33+
},
34+
35+
// A manually-constructed thenable that works even if calls precede resolve/reject
36+
promise: {
37+
then: function() {
38+
if ( !adopted ) {
39+
if ( !promised ) {
40+
promised = jQuery.Deferred();
41+
}
42+
return promised.then.apply( promised, arguments );
43+
}
44+
return adopted.then.apply( adopted, arguments );
45+
}
46+
}
47+
};
48+
49+
return obj;
50+
};
51+
} );

0 commit comments

Comments
 (0)