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
This commit is contained in:
Richard Gibson 2016-04-14 23:59:30 -04:00
parent 0bd98b1b13
commit 356a3bccb0
6 changed files with 341 additions and 225 deletions

View File

@ -4,10 +4,20 @@ module.exports = function( grunt ) {
var spawnTest = require( "./lib/spawn_test.js" );
grunt.registerTask( "promises_aplus_tests", function() {
grunt.registerTask( "promises_aplus_tests",
[ "promises_aplus_tests_deferred", "promises_aplus_tests_when" ] );
grunt.registerTask( "promises_aplus_tests_deferred", function() {
spawnTest( this.async(),
"./node_modules/.bin/promises-aplus-tests",
"test/promises_aplus_adapter.js"
"test/promises_aplus_adapter_deferred.js"
);
} );
grunt.registerTask( "promises_aplus_tests_when", function() {
spawnTest( this.async(),
"./node_modules/.bin/promises-aplus-tests",
"test/promises_aplus_adapter_when.js"
);
} );
};

View File

@ -13,6 +13,38 @@ function Thrower( ex ) {
throw ex;
}
function adoptValue( value, resolve, reject ) {
var method;
try {
// Check for promise aspect first to privilege synchronous behavior
if ( value && jQuery.isFunction( ( method = value.promise ) ) ) {
method.call( value ).done( resolve ).fail( reject );
// Other thenables
} else if ( value && jQuery.isFunction( ( method = value.then ) ) ) {
method.call( value, resolve, reject );
// Other non-thenables
} else {
// Support: Android 4.0 only
// Strict mode functions invoked without .call/.apply get global-object context
resolve.call( undefined, value );
}
// For Promises/A+, convert exceptions into rejections
// Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in
// Deferred#then to conditionally suppress rejection.
} catch ( /*jshint -W002 */ value ) {
// Support: Android 4.0 only
// Strict mode functions invoked without .call/.apply get global-object context
reject.call( undefined, value );
}
}
jQuery.extend( {
Deferred: function( func ) {
@ -305,67 +337,45 @@ jQuery.extend( {
},
// Deferred helper
when: function() {
var method, resolveContexts,
i = 0,
when: function( singleValue ) {
var
// count of uncompleted subordinates
remaining = arguments.length,
// count of unprocessed arguments
i = remaining,
// subordinate fulfillment data
resolveContexts = Array( i ),
resolveValues = slice.call( arguments ),
length = resolveValues.length,
// the count of uncompleted subordinates
remaining = length,
// the master Deferred.
// the master Deferred
master = jQuery.Deferred(),
// Update function for both resolving subordinates
// subordinate callback factory
updateFunc = function( i ) {
return function( value ) {
resolveContexts[ i ] = this;
resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
if ( !( --remaining ) ) {
master.resolveWith(
resolveContexts.length === 1 ? resolveContexts[ 0 ] : resolveContexts,
resolveValues
);
master.resolveWith( resolveContexts, resolveValues );
}
};
};
// Add listeners to promise-like subordinates; treat others as resolved
if ( length > 0 ) {
resolveContexts = new Array( length );
for ( ; i < length; i++ ) {
// Single- and empty arguments are adopted like Promise.resolve
if ( remaining <= 1 ) {
adoptValue( singleValue, master.resolve, master.reject );
// jQuery.Deferred - treated specially to get resolve-sync behavior
if ( resolveValues[ i ] &&
jQuery.isFunction( ( method = resolveValues[ i ].promise ) ) ) {
method.call( resolveValues[ i ] )
.done( updateFunc( i ) )
.fail( master.reject );
// Other thenables
} else if ( resolveValues[ i ] &&
jQuery.isFunction( ( method = resolveValues[ i ].then ) ) ) {
method.call(
resolveValues[ i ],
updateFunc( i ),
master.reject
);
} else {
// Support: Android 4.0 only
// Strict mode functions invoked without .call/.apply get global-object context
updateFunc( i ).call( undefined, resolveValues[ i ] );
}
}
// If we're not waiting on anything, resolve the master
} else {
master.resolveWith();
// Use .then() to unwrap secondary thenables (cf. gh-3000)
return master.then();
}
// Multiple arguments are aggregated like Promise.all array elements
while ( i-- ) {
adoptValue( resolveValues[ i ], updateFunc( i ), master.reject );
}
return master.promise();
}
} );

View File

@ -0,0 +1,51 @@
/* jshint node: true */
"use strict";
require( "jsdom" ).env( "", function( errors, window ) {
if ( errors ) {
console.error( errors );
return;
}
var jQuery = require( ".." )( window );
exports.deferred = function() {
var adopted, promised,
obj = {
resolve: function() {
if ( !adopted ) {
adopted = jQuery.when.apply( jQuery, arguments );
if ( promised ) {
adopted.then( promised.resolve, promised.reject );
}
}
return adopted;
},
reject: function( value ) {
if ( !adopted ) {
adopted = jQuery.when( jQuery.Deferred().reject( value ) );
if ( promised ) {
adopted.then( promised.resolve, promised.reject );
}
}
return adopted;
},
// A manually-constructed thenable that works even if calls precede resolve/reject
promise: {
then: function() {
if ( !adopted ) {
if ( !promised ) {
promised = jQuery.Deferred();
}
return promised.then.apply( promised, arguments );
}
return adopted.then.apply( adopted, arguments );
}
}
};
return obj;
};
} );

View File

@ -761,11 +761,30 @@ QUnit.test( "jQuery.Deferred - notify and resolve", function( assert ) {
} );
} );
QUnit.test( "jQuery.when", function( assert ) {
assert.expect( 37 );
QUnit.test( "jQuery.when(nonThenable) - like Promise.resolve", function( assert ) {
"use strict";
assert.expect( 44 );
var
// Support: Android 4.0 only
// Strict mode functions invoked without .call/.apply get global-object context
defaultContext = (function getDefaultContext() { return this; }).call(),
done = assert.async( 20 );
jQuery.when()
.done( function( resolveValue ) {
assert.strictEqual( resolveValue, undefined, "Resolved .done with no arguments" );
assert.strictEqual( this, defaultContext, "Default .done context with no arguments" );
} )
.then( function( resolveValue ) {
assert.strictEqual( resolveValue, undefined, "Resolved .then with no arguments" );
assert.strictEqual( this, defaultContext, "Default .then context with no arguments" );
} );
// Some other objects
jQuery.each( {
"an empty string": "",
"a non-empty string": "some string",
@ -778,51 +797,136 @@ QUnit.test( "jQuery.when", function( assert ) {
"a plain object": {},
"an array": [ 1, 2, 3 ]
}, function( message, value ) {
assert.ok(
jQuery.isFunction(
jQuery.when( value ).done( function( resolveValue ) {
assert.strictEqual( this, window, "Context is the global object with " + message );
assert.strictEqual( resolveValue, value, "Test the promise was resolved with " + message );
} ).promise
),
"Test " + message + " triggers the creation of a new Promise"
);
} );
assert.ok(
jQuery.isFunction(
jQuery.when().done( function( resolveValue ) {
assert.strictEqual( this, window, "Test the promise was resolved with window as its context" );
assert.strictEqual( resolveValue, undefined, "Test the promise was resolved with no parameter" );
} ).promise
),
"Test calling when with no parameter triggers the creation of a new Promise"
);
var cache,
context = {};
jQuery.when( jQuery.Deferred().resolveWith( context ) ).done( function() {
assert.strictEqual( this, context, "when( promise ) propagates context" );
} );
jQuery.each( [ 1, 2, 3 ], function( k, i ) {
jQuery.when( cache || jQuery.Deferred( function() {
this.resolve( i );
} )
).done( function( value ) {
assert.strictEqual( value, 1, "Function executed" + ( i > 1 ? " only once" : "" ) );
cache = value;
} );
var code = "jQuery.when( " + message + " )",
onFulfilled = function( method ) {
var call = code + "." + method;
return function( resolveValue ) {
assert.strictEqual( resolveValue, value, call + " resolve" );
assert.strictEqual( this, defaultContext, call + " context" );
done();
};
},
onRejected = function( method ) {
var call = code + "." + method;
return function() {
assert.ok( false, call + " reject" );
done();
};
};
jQuery.when( value )
.done( onFulfilled( "done" ) )
.fail( onRejected( "done" ) )
.then( onFulfilled( "then" ), onRejected( "then" ) );
} );
} );
QUnit.test( "jQuery.when - joined", function( assert ) {
QUnit.test( "jQuery.when(thenable) - like Promise.resolve", function( assert ) {
"use strict";
assert.expect( 81 );
assert.expect( 56 );
var deferreds = {
var slice = [].slice,
sentinel = { context: "explicit" },
eventuallyFulfilled = jQuery.Deferred().notify( true ),
eventuallyRejected = jQuery.Deferred().notify( true ),
inputs = {
promise: Promise.resolve( true ),
rejectedPromise: Promise.reject( false ),
deferred: jQuery.Deferred().resolve( true ),
eventuallyFulfilled: eventuallyFulfilled,
secondaryFulfilled: jQuery.Deferred().resolve( eventuallyFulfilled ),
multiDeferred: jQuery.Deferred().resolve( "foo", "bar" ),
deferredWith: jQuery.Deferred().resolveWith( sentinel, [ true ] ),
multiDeferredWith: jQuery.Deferred().resolveWith( sentinel, [ "foo", "bar" ] ),
rejectedDeferred: jQuery.Deferred().reject( false ),
eventuallyRejected: eventuallyRejected,
secondaryRejected: jQuery.Deferred().resolve( eventuallyRejected ),
multiRejectedDeferred: jQuery.Deferred().reject( "baz", "quux" ),
rejectedDeferredWith: jQuery.Deferred().rejectWith( sentinel, [ false ] ),
multiRejectedDeferredWith: jQuery.Deferred().rejectWith( sentinel, [ "baz", "quux" ] )
},
contexts = {
deferredWith: sentinel,
multiDeferredWith: sentinel,
rejectedDeferredWith: sentinel,
multiRejectedDeferredWith: sentinel
},
willSucceed = {
promise: [ true ],
deferred: [ true ],
eventuallyFulfilled: [ true ],
secondaryFulfilled: [ true ],
multiDeferred: [ "foo", "bar" ],
deferredWith: [ true ],
multiDeferredWith: [ "foo", "bar" ]
},
willError = {
rejectedPromise: [ false ],
rejectedDeferred: [ false ],
eventuallyRejected: [ false ],
secondaryRejected: [ false ],
multiRejectedDeferred: [ "baz", "quux" ],
rejectedDeferredWith: [ false ],
multiRejectedDeferredWith: [ "baz", "quux" ]
},
// Support: Android 4.0 only
// Strict mode functions invoked without .call/.apply get global-object context
defaultContext = (function getDefaultContext() { return this; }).call(),
done = assert.async( 28 );
jQuery.each( inputs, function( message, value ) {
var code = "jQuery.when( " + message + " )",
shouldResolve = willSucceed[ message ],
shouldError = willError[ message ],
context = contexts[ message ] || defaultContext,
onFulfilled = function( method ) {
var call = code + "." + method;
return function() {
if ( shouldResolve ) {
assert.deepEqual( slice.call( arguments ), shouldResolve,
call + " resolve" );
assert.strictEqual( this, context, call + " context" );
} else {
assert.ok( false, call + " resolve" );
}
done();
};
},
onRejected = function( method ) {
var call = code + "." + method;
return function() {
if ( shouldError ) {
assert.deepEqual( slice.call( arguments ), shouldError, call + " reject" );
assert.strictEqual( this, context, call + " context" );
} else {
assert.ok( false, call + " reject" );
}
done();
};
};
jQuery.when( value )
.done( onFulfilled( "done" ) )
.fail( onRejected( "done" ) )
.then( onFulfilled( "then" ), onRejected( "then" ) );
} );
setTimeout( function() {
eventuallyFulfilled.resolve( true );
eventuallyRejected.reject( false );
}, 50 );
} );
QUnit.test( "jQuery.when(a, b) - like Promise.all", function( assert ) {
"use strict";
assert.expect( 196 );
var slice = [].slice,
deferreds = {
rawValue: 1,
fulfilled: jQuery.Deferred().resolve( 1 ),
rejected: jQuery.Deferred().reject( 0 ),
@ -842,46 +946,91 @@ QUnit.test( "jQuery.when - joined", function( assert ) {
eventuallyRejected: true,
rejectedStandardPromise: true
},
counter = 49,
// Support: Android 4.0 only
// Strict mode functions invoked without .call/.apply get global-object context
expectedContext = (function() { "use strict"; return this; }).call();
defaultContext = (function getDefaultContext() { return this; }).call(),
QUnit.stop();
done = assert.async( 98 );
function restart() {
if ( !--counter ) {
QUnit.start();
}
}
jQuery.each( deferreds, function( id1, defer1 ) {
jQuery.each( deferreds, function( id2, defer2 ) {
var shouldResolve = willSucceed[ id1 ] && willSucceed[ id2 ],
jQuery.each( deferreds, function( id1, v1 ) {
jQuery.each( deferreds, function( id2, v2 ) {
var code = "jQuery.when( " + id1 + ", " + id2 + " )",
shouldResolve = willSucceed[ id1 ] && willSucceed[ id2 ],
shouldError = willError[ id1 ] || willError[ id2 ],
expected = shouldResolve ? [ 1, 1 ] : [ 0, undefined ],
code = "jQuery.when( " + id1 + ", " + id2 + " )";
expected = shouldResolve ? [ 1, 1 ] : [ 0 ],
context = shouldResolve ? [ defaultContext, defaultContext ] : defaultContext,
onFulfilled = function( method ) {
var call = code + "." + method;
return function() {
if ( shouldResolve ) {
assert.deepEqual( slice.call( arguments ), expected,
call + " resolve" );
assert.deepEqual( this, context, code + " context" );
} else {
assert.ok( false, call + " resolve" );
}
done();
};
},
onRejected = function( method ) {
var call = code + "." + method;
return function() {
if ( shouldError ) {
assert.deepEqual( slice.call( arguments ), expected, call + " reject" );
assert.deepEqual( this, context, code + " context" );
} else {
assert.ok( false, call + " reject" );
}
done();
};
};
jQuery.when( defer1, defer2 ).done( function( a, b ) {
if ( shouldResolve ) {
assert.deepEqual( [ a, b ], expected, code + " => resolve" );
assert.strictEqual( this[ 0 ], expectedContext, code + " => context[0] OK" );
assert.strictEqual( this[ 1 ], expectedContext, code + " => context[1] OK" );
} else {
assert.ok( false, code + " => resolve" );
}
} ).fail( function( a, b ) {
if ( shouldError ) {
assert.deepEqual( [ a, b ], expected, code + " => reject" );
} else {
assert.ok( false, code + " => reject" );
}
} ).always( restart );
jQuery.when( v1, v2 )
.done( onFulfilled( "done" ) )
.fail( onRejected( "done" ) )
.then( onFulfilled( "then" ), onRejected( "then" ) );
} );
} );
setTimeout( function() {
deferreds.eventuallyFulfilled.resolve( 1 );
deferreds.eventuallyRejected.reject( 0 );
}, 50 );
} );
QUnit.test( "jQuery.when - always returns a new promise", function( assert ) {
assert.expect( 42 );
jQuery.each( {
"no arguments": [],
"non-thenable": [ "foo" ],
"promise": [ Promise.resolve( "bar" ) ],
"rejected promise": [ Promise.reject( "bar" ) ],
"deferred": [ jQuery.Deferred().resolve( "baz" ) ],
"rejected deferred": [ jQuery.Deferred().reject( "baz" ) ],
"multi-resolved deferred": [ jQuery.Deferred().resolve( "qux", "quux" ) ],
"multiple non-thenables": [ "corge", "grault" ],
"multiple deferreds": [
jQuery.Deferred().resolve( "garply" ),
jQuery.Deferred().resolve( "waldo" )
]
}, function( label, args ) {
var result = jQuery.when.apply( jQuery, args );
assert.ok( jQuery.isFunction( result.then ), "Thenable returned from " + label );
assert.strictEqual( result.resolve, undefined, "Non-deferred returned from " + label );
assert.strictEqual( result.promise(), result, "Promise returned from " + label );
jQuery.each( args, function( i, arg ) {
assert.notStrictEqual( result, arg, "Returns distinct from arg " + i + " of " + label );
if ( arg.promise ) {
assert.notStrictEqual( result, arg.promise(),
"Returns distinct from promise of arg " + i + " of " + label );
}
} );
} );
deferreds.eventuallyFulfilled.resolve( 1 );
deferreds.eventuallyRejected.reject( 0 );
} );
QUnit.test( "jQuery.when - notify does not affect resolved", function( assert ) {
@ -900,107 +1049,3 @@ QUnit.test( "jQuery.when - notify does not affect resolved", function( assert )
assert.ok( false, "Error on resolve" );
} );
} );
QUnit.test( "jQuery.when - filtering", function( assert ) {
assert.expect( 2 );
function increment( x ) {
return x + 1;
}
QUnit.stop();
jQuery.when(
jQuery.Deferred().resolve( 3 ).then( increment ),
jQuery.Deferred().reject( 5 ).then( null, increment )
).done( function( four, six ) {
assert.strictEqual( four, 4, "resolved value incremented" );
assert.strictEqual( six, 6, "rejected value incremented" );
QUnit.start();
} );
} );
QUnit.test( "jQuery.when - exceptions", function( assert ) {
assert.expect( 2 );
function woops() {
throw "exception thrown";
}
QUnit.stop();
jQuery.Deferred().resolve().then( woops ).fail( function( doneException ) {
assert.strictEqual( doneException, "exception thrown", "throwing in done handler" );
jQuery.Deferred().reject().then( null, woops ).fail( function( failException ) {
assert.strictEqual( failException, "exception thrown", "throwing in fail handler" );
QUnit.start();
} );
} );
} );
QUnit.test( "jQuery.when - chaining", function( assert ) {
assert.expect( 4 );
var defer = jQuery.Deferred();
function chain() {
return defer;
}
function chainStandard() {
return Promise.resolve( "std deferred" );
}
QUnit.stop();
jQuery.when(
jQuery.Deferred().resolve( 3 ).then( chain ),
jQuery.Deferred().reject( 5 ).then( null, chain ),
jQuery.Deferred().resolve( 3 ).then( chainStandard ),
jQuery.Deferred().reject( 5 ).then( null, chainStandard )
).done( function( v1, v2, s1, s2 ) {
assert.strictEqual( v1, "other deferred", "chaining in done handler" );
assert.strictEqual( v2, "other deferred", "chaining in fail handler" );
assert.strictEqual( s1, "std deferred", "chaining thenable in done handler" );
assert.strictEqual( s2, "std deferred", "chaining thenable in fail handler" );
QUnit.start();
} );
defer.resolve( "other deferred" );
} );
QUnit.test( "jQuery.when - solitary thenables", function( assert ) {
assert.expect( 1 );
var done = assert.async(),
rejected = new Promise( function( resolve, reject ) {
setTimeout( function() {
reject( "rejected" );
}, 100 );
} );
jQuery.when( rejected ).then(
function() {
assert.ok( false, "Rejected, solitary, non-Deferred thenable should not resolve" );
done();
},
function() {
assert.ok( true, "Rejected, solitary, non-Deferred thenable rejected properly" );
done();
}
);
} );
QUnit.test( "jQuery.when does not reuse a solitary jQuery Deferred (gh-2018)", function( assert ) {
assert.expect( 2 );
var defer = jQuery.Deferred().resolve(),
promise = jQuery.when( defer );
assert.equal( promise.state(), "resolved", "Master Deferred is immediately resolved" );
assert.notStrictEqual( defer.promise(), promise, "jQuery.when returns the master deferred's promise" );
} );

View File

@ -1028,7 +1028,7 @@ jQuery.each( {
jQuery( elem ).remove();
} );
this.clock.tick( 50 );
this.clock.tick( 100 );
} );
} );
} );