diff options
author | Richard Gibson <richard.gibson@gmail.com> | 2014-12-29 14:14:13 -0500 |
---|---|---|
committer | Richard Gibson <richard.gibson@gmail.com> | 2015-03-20 02:00:17 -0400 |
commit | 555a50d340706e3e1e0de09231050493d0ad841e (patch) | |
tree | 2348f102acac1694287133bf7bdedbdf47d55dc9 /src/deferred.js | |
parent | e22ef5d9017c44cad97ae541fefce76cc455edcb (diff) | |
download | jquery-555a50d340706e3e1e0de09231050493d0ad841e.tar.gz jquery-555a50d340706e3e1e0de09231050493d0ad841e.zip |
Deferred: Backwards-compatible standards interoperability
Fixes gh-1722
Closes gh-1996
Diffstat (limited to 'src/deferred.js')
-rw-r--r-- | src/deferred.js | 248 |
1 files changed, 221 insertions, 27 deletions
diff --git a/src/deferred.js b/src/deferred.js index 939b58f37..2522c693e 100644 --- a/src/deferred.js +++ b/src/deferred.js @@ -4,14 +4,24 @@ define([ "./callbacks" ], function( jQuery, slice ) { +function Identity( v ) { + return v; +} +function Thrower( ex ) { + throw ex; +} + jQuery.extend({ Deferred: function( func ) { var tuples = [ - // action, add listener, listener list, final state - [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], - [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], - [ "notify", "progress", jQuery.Callbacks("memory") ] + // action, add listener, callbacks, .then handlers, final state + [ "resolve", "done", jQuery.Callbacks("once memory"), + jQuery.Callbacks("once memory"), "resolved" ], + [ "reject", "fail", jQuery.Callbacks("once memory"), + jQuery.Callbacks("once memory"), "rejected" ], + [ "notify", "progress", jQuery.Callbacks("memory"), + jQuery.Callbacks("memory") ] ], state = "pending", promise = { @@ -22,12 +32,16 @@ jQuery.extend({ deferred.done( arguments ).fail( arguments ); return this; }, - then: function( /* fnDone, fnFail, fnProgress */ ) { + // Keep pipe for back-compat + pipe: function( /* fnDone, fnFail, fnProgress */ ) { var fns = arguments; + return jQuery.Deferred(function( newDefer ) { jQuery.each( tuples, function( i, tuple ) { var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; - // deferred[ done | fail | progress ] for forwarding actions to newDefer + // deferred.done(function() { bind to newDefer or newDefer.resolve }) + // deferred.fail(function() { bind to newDefer or newDefer.reject }) + // deferred.progress(function() { bind to newDefer or newDefer.notify }) deferred[ tuple[1] ](function() { var returned = fn && fn.apply( this, arguments ); if ( returned && jQuery.isFunction( returned.promise ) ) { @@ -46,6 +60,157 @@ jQuery.extend({ fns = null; }).promise(); }, + then: function( onFulfilled, onRejected, onProgress ) { + var maxDepth = 0; + function resolve( depth, deferred, handler, special ) { + return function() { + var that = this === promise ? undefined : this, + args = arguments, + mightThrow = function() { + var returned, then; + + // Support: Promises/A+ section 2.3.3.3.3 + // https://promisesaplus.com/#point-59 + // Ignore double-resolution attempts + if ( depth < maxDepth ) { + return; + } + + returned = handler.apply( that, args ); + + // Support: Promises/A+ section 2.3.1 + // https://promisesaplus.com/#point-48 + if ( returned === deferred.promise() ) { + throw new TypeError( "Thenable self-resolution" ); + } + + // Support: Promises/A+ sections 2.3.3.1, 3.5 + // https://promisesaplus.com/#point-54 + // https://promisesaplus.com/#point-75 + // Retrieve `then` only once + then = returned && + + // Support: Promises/A+ section 2.3.4 + // https://promisesaplus.com/#point-64 + // Only check objects and functions for thenability + ( typeof returned === "object" || + typeof returned === "function" ) && + returned.then; + + // Handle a returned thenable + if ( jQuery.isFunction( then ) ) { + // Special processors (notify) just wait for resolution + if ( special ) { + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ) + ); + + // Normal processors (resolve) also hook into progress + } else { + + // ...and disregard older resolution values + maxDepth++; + + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ), + resolve( maxDepth, deferred, Identity, + deferred.notify ) + ); + } + + // Handle all other returned values + } else { + // Only substitue handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Identity ) { + that = undefined; + args = [ returned ]; + } + + // Process the value(s) + // Default process is resolve + ( special || deferred.resolveWith )( + that || deferred.promise(), args ); + } + }, + + // Only normal processors (resolve) catch and reject exceptions + process = special ? + mightThrow : + function() { + try { + mightThrow(); + } catch ( e ) { + + // Support: Promises/A+ section 2.3.3.3.4.1 + // https://promisesaplus.com/#point-61 + // Ignore post-resolution exceptions + if ( depth + 1 >= maxDepth ) { + // Only substitue handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Thrower ) { + that = undefined; + args = [ e ]; + } + + deferred.rejectWith( that || deferred.promise(), + args ); + } + } + }; + + // Support: Promises/A+ section 2.3.3.3.1 + // https://promisesaplus.com/#point-57 + // Re-resolve promises immediately to dodge false rejection from + // subsequent errors + if ( depth ) { + process(); + } else { + setTimeout( process ); + } + }; + } + + return jQuery.Deferred(function( newDefer ) { + // fulfilled_handlers.add( ... ) + tuples[ 0 ][ 3 ].add( + resolve( + 0, + newDefer, + jQuery.isFunction( onFulfilled ) ? + onFulfilled : + Identity + ) + ); + + // rejected_handlers.add( ... ) + tuples[ 1 ][ 3 ].add( + resolve( + 0, + newDefer, + jQuery.isFunction( onRejected ) ? + onRejected : + Thrower + ) + ); + + // progress_handlers.add( ... ) + tuples[ 2 ][ 3 ].add( + resolve( + 0, + newDefer, + jQuery.isFunction( onProgress ) ? + onProgress : + Identity, + newDefer.notifyWith + ) + ); + }).promise(); + }, // Get a promise for this deferred // If obj is provided, the promise aspect is added to the object promise: function( obj ) { @@ -54,32 +219,50 @@ jQuery.extend({ }, deferred = {}; - // Keep pipe for back-compat - promise.pipe = promise.then; - // Add list-specific methods jQuery.each( tuples, function( i, tuple ) { var list = tuple[ 2 ], - stateString = tuple[ 3 ]; + stateString = tuple[ 4 ]; - // promise[ done | fail | progress ] = list.add + // promise.done = list.add + // promise.fail = list.add + // promise.progress = list.add promise[ tuple[1] ] = list.add; // Handle state if ( stateString ) { - list.add(function() { - // state = [ resolved | rejected ] - state = stateString; + list.add( + function() { + // state = "resolved" (i.e., fulfilled) + // state = "rejected" + state = stateString; + }, + + // rejected_callbacks.disable + // fulfilled_callbacks.disable + tuples[ i ^ 1 ][ 2 ].disable, - // [ reject_list | resolve_list ].disable; progress_list.lock - }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + // progress_callbacks.lock + tuples[ 2 ][ 2 ].lock + ); } - // deferred[ resolve | reject | notify ] + // fulfilled_handlers.fire + // rejected_handlers.fire + // progress_handlers.fire + list.add( tuple[ 3 ].fire ); + + // deferred.resolve = function() { deferred.resolveWith(...) } + // deferred.reject = function() { deferred.rejectWith(...) } + // deferred.notify = function() { deferred.notifyWith(...) } deferred[ tuple[0] ] = function() { deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); return this; }; + + // deferred.resolveWith = list.fireWith + // deferred.rejectWith = list.fireWith + // deferred.notifyWith = list.fireWith deferred[ tuple[0] + "With" ] = list.fireWith; }); @@ -97,7 +280,8 @@ jQuery.extend({ // Deferred helper when: function( subordinate /* , ..., subordinateN */ ) { - var i = 0, + var method, + i = 0, resolveValues = slice.call( arguments ), length = resolveValues.length, @@ -107,7 +291,7 @@ jQuery.extend({ // the master Deferred. // If resolveValues consist of only a single Deferred, just use that. - deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + master = remaining === 1 ? subordinate : jQuery.Deferred(), // Update function for both resolve and progress values updateFunc = function( i, contexts, values ) { @@ -115,13 +299,12 @@ jQuery.extend({ contexts[ i ] = this; values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; if ( values === progressValues ) { - deferred.notifyWith( contexts, values ); + master.notifyWith( contexts, values ); } else if ( !( --remaining ) ) { - deferred.resolveWith( contexts, values ); + master.resolveWith( contexts, values ); } }; }, - progressValues, progressContexts, resolveContexts; // Add listeners to Deferred subordinates; treat others as resolved @@ -130,11 +313,22 @@ jQuery.extend({ progressContexts = new Array( length ); resolveContexts = new Array( length ); for ( ; i < length; i++ ) { - if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { - resolveValues[ i ].promise() + if ( resolveValues[ i ] && + jQuery.isFunction( (method = resolveValues[ i ].promise) ) ) { + + method.call( resolveValues[ i ] ) .progress( updateFunc( i, progressContexts, progressValues ) ) .done( updateFunc( i, resolveContexts, resolveValues ) ) - .fail( deferred.reject ); + .fail( master.reject ); + } else if ( resolveValues[ i ] && + jQuery.isFunction( (method = resolveValues[ i ].then) ) ) { + + method.call( + resolveValues[ i ], + updateFunc( i, resolveContexts, resolveValues ), + master.reject, + updateFunc( i, progressContexts, progressValues ) + ); } else { --remaining; } @@ -143,10 +337,10 @@ jQuery.extend({ // If we're not waiting on anything, resolve the master if ( !remaining ) { - deferred.resolveWith( resolveContexts, resolveValues ); + master.resolveWith( resolveContexts, resolveValues ); } - return deferred.promise(); + return master.promise(); } }); |