Skip to content
Permalink
Browse files
Added deferred to core. Used internally for DOM readyness and ajax ca…
…llbacks.
  • Loading branch information
jaubourg authored and jaubourg committed Dec 31, 2010
1 parent c1625f6 commit 5bacb53
Show file tree
Hide file tree
Showing 3 changed files with 458 additions and 187 deletions.
@@ -59,9 +59,9 @@ var jQuery = function( selector, context ) {

// Has the ready events already been bound?
readyBound = false,

// The functions to execute on DOM ready
readyList = [],
// The deferred used on DOM ready
readyList,

// The ready event handler
DOMContentLoaded,
@@ -75,7 +75,10 @@ var jQuery = function( selector, context ) {
indexOf = Array.prototype.indexOf,

// [[Class]] -> type pairs
class2type = {};
class2type = {},

// Marker for deferred
deferredMarker = [];

jQuery.fn = jQuery.prototype = {
init: function( selector, context ) {
@@ -252,23 +255,13 @@ jQuery.fn = jQuery.prototype = {
each: function( callback, args ) {
return jQuery.each( this, callback, args );
},

ready: function( fn ) {
ready: function() {
// Attach the listeners
jQuery.bindReady();

// If the DOM is already ready
if ( jQuery.isReady ) {
// Execute the function immediately
fn.call( document, jQuery );

// Otherwise, remember the function for later
} else if ( readyList ) {
// Add the function to the wait list
readyList.push( fn );
}

return this;

// Change ready & apply
return ( jQuery.fn.ready = readyList.then ).apply( this , arguments );
},

eq: function( i ) {
@@ -415,23 +408,11 @@ jQuery.extend({
}

// If there are functions bound, to execute
if ( readyList ) {
// Execute all of them
var fn,
i = 0,
ready = readyList;

// Reset the list of functions
readyList = null;

while ( (fn = ready[ i++ ]) ) {
fn.call( document, jQuery );
}

// Trigger any bound ready events
if ( jQuery.fn.trigger ) {
jQuery( document ).trigger( "ready" ).unbind( "ready" );
}
readyList.fire( document , [ jQuery ] );

// Trigger any bound ready events
if ( jQuery.fn.trigger ) {
jQuery( document ).trigger( "ready" ).unbind( "ready" );
}
}
},
@@ -800,6 +781,160 @@ jQuery.extend({
now: function() {
return (new Date()).getTime();
},

// Create a simple deferred (one callbacks list)
_deferred: function( cancellable ) {

// cancellable by default
cancellable = cancellable !== false;

var // callbacks list
callbacks = [],
// stored [ context , args ]
fired,
// to avoid firing when already doing so
firing,
// flag to know if the deferred has been cancelled
cancelled,
// the deferred itself
deferred = {

// then( f1, f2, ...)
then: function() {

This comment has been minimized.

Copy link
@kriszyp

kriszyp Dec 31, 2010

It's totally awesome to see Deferreds/promises added to jQuery with API basically compatible with other libraries, great addition! However, a slight nit: in all other libraries that implement thenable promises/deferreds (node-promise, promised-io, Dojo, ringo, etc.), the signature is usually then(callbackForSuccess, callbackForFail). How do you register a fail handler/callback with this API (to catch errors or when fail() is called)? I like your approach of allowing for adding multiple callbacks in a single call, and this would still be possible by allowing an array to be used as the first arg while reserving the second arg for the error callback (at least in the Deferred impl, vs the _Deferred impl).

This comment has been minimized.

Copy link
@jaubourg

jaubourg Dec 31, 2010

Member

// Attach callbacks

var deferred = jQuery.Deferred().then( thenCallback1 , thenCallback2, ... ).fail( failCallback1 , failCallback2, ...)

// Resolve

deferred.resolve( value ); // Will call all attached "then" callbacks

// Reject

deferred.reject( error ); // Will call all attached "fail" callbacks

I've looked the APIs around and I have to say I don't fancy the then( callbackForSuccess , callbackForError ) signature. I find deferred.then( null , errorCallback ) as a mean to attach an error callback to be quite unnatural, convoluted and plain confusing. Really, deferred.fail( errorCallback ) seems much simpler and straight to the point.

This comment has been minimized.

Copy link
@kriszyp

kriszyp Dec 31, 2010

Oh, I see, fail() registers the error handler, I didn't realize that, I had assumed fail() caused it to fail (seemed the natural meaning of fail), I guess fireReject() does that. Anyway, certainly not suggesting that you should remove fail() (now that I understand it), it is looks quite reasonable to have a specific function for error handling. However, I am suggesting that while that are a lot of different preferences on how callback registration should look (some like spelling it when or addCallback, some like single arg only, etc) that there is an enormous opportunity here for interoperability with existing libraries here that seems more valuable than any one's personal preference. And you wouldn't be losing much by accepting a second arg, you can still provide a list of callbacks with arrays (and I think providing a list of callbacks is actually pretty rare from my experience in promises, people usually just put multiple actions, many of which are more complicated than a simple function call into a single anon function):

jQuery.Deferred().then([thenCallback1 , thenCallback2, ... ]).fail( failCallback1 , failCallback2, ...)

and for those that are counting bytes, the second arg as error handler gives them an option for a net win:

jQuery.Deferred().then([thenCallback1 , thenCallback2, ... ], [ failCallback1 , failCallback2, ...])

But again, not appealing on the basis of API preference (using then() and fail() does read nicely), but on the basis of the awesome opportunity to use existing libraries with this new jQuery construct.

This comment has been minimized.

Copy link
@kriszyp

kriszyp Dec 31, 2010

Also, FWIW, here it the CommonJS promise proposal that others have followed: http://wiki.commonjs.org/wiki/Promises/A (could ignore the get() and call(), that isn't really used much, just then() part).
But, I am mainly just interested in interoperability, if you really believe strongly in then()/fail(), would you want to propose that to CommonJS?

This comment has been minimized.

Copy link
@csnover

csnover Dec 31, 2010

Member

kriszyp, there seem to be 3 proposals right now for CommonJS deferreds: one implemented by dojo, one implemented by Narwhal, and one implemented by FuturesJS. Promises/B is the only one currently listed on the CommonJS home page as an in-progress draft. Can you describe the current state of each of these proposed standards, and why we would want to choose, say, Promises/A instead of Promises/B or Promises/KISS?

This comment has been minimized.

Copy link
@kriszyp

kriszyp Dec 31, 2010

Promises/A is designed to be the minimal API to facilitate promise producer and consumer interaction. It only requires the presence of a then(successHandler, errorHandler) function on promise objects. I wasn't sure if you had already looked at the proposal before this patch since you almost exactly match the proposal (same name, only a slight difference in argument treatment).

Promises/B was designed from object capability security perspective with the goal avoiding direct method calls on a promise to always have a layer of indirection to ensure that errors aren't thrown, etc. I don't think the inconvenience of this extra layer would fit well jQuery's style, and the API is far too large to realistically be considered for inclusion in jQuery. It is worth noting that third party libraries could (and do) easily implement the Promises/B API for all promises that conform to Promises/A. Thus if jQuery's Deferreds/promises' then() function matches Promises/A, libraries (existing ones included) that provide Promises/B functionality could immediately be used with jQuery's promises. With the current then() implement they would already somewhat work, just that the error handling would be messed up.

Promises/KISS is a smaller API than /B, but bigger than /A. That spec seems to rely a CommonJS module system, and so doesn't really apply to jQuery.

I believe that Promises/A is the only one small enough to appropriately fit in jQuery (you essentially almost have it). Promises/A also is the most widely implemented. The page lists a few impls, but it is also in Dojo, and I think RingoJS uses it.

Hope that helps.

This comment has been minimized.

Copy link
@jaubourg

jaubourg Jan 1, 2011

Member

I'd be more than happy to make a proposition but I have no clue how.

I was well aware of Promises/A and I've looked at existing implementations.

This comment has been minimized.

Copy link
@rmurphey

rmurphey Jan 1, 2011

@kriszyp, thanks for the terrific differentiation of /A, /B, and /KISS. Can you point to any other implementations of Promises/A besides Dojo's (or plans you're aware of)? I think that would help clarify what jQuery would be choosing to be interoperable with (or not interoperable with, as the case may be).

@everyone

It sounds like the bottom line is the question of whether .then() should accept two callbacks or one.

I'm +1 on interoperability (and thus allowing two callbacks to then()), but I wouldn't mind seeing .fail() stick around as well, as it is pleasantly readable. Is there a reason not to do both? More importantly, is there a reason to make jQuery's dfd implementation specifically not adhere to Promises/A? It sounds like this decision would also mean that it would not be compatible extending to Promises/B. I realize that all of these promise specs are still emerging, but I'm not convinced that two callbacks to then() is so fundamentally bad that it shouldn't be allowed.

This comment has been minimized.

Copy link
@temp01

temp01 Jan 1, 2011

Contributor

I'm with Kris on this; if this minor change means better compatibility with other implementations, +1 to that.

This comment has been minimized.

Copy link
@kriszyp

kriszyp Jan 1, 2011

@rmurphey The implementations I am aware of that use Promises/A (besides Dojo) include promised-io (promise library for NodeJS) and then the CommonJS JSGI interface (HTTP interface like WSGI) uses Promises/A for async, and so Promises/A style promises are consumed by Jack (Rack for JavaScript), RingoJS, and Node-JSGI.

@jaubourg: The CommonJS mailing list is a google group (commonjs), and you could propose there, and if you wanted you could write up a spec/proposal on the wiki (http://wiki.commonjs.org/wiki/Promises, just create an account).

This comment has been minimized.

Copy link
@temp01

temp01 Jan 3, 2011

Contributor

To anyone following this.. http://jaubourg.net/38261410


if ( ! cancelled ) {

var args = arguments,
i,
type,
_fired;

if ( fired ) {
_fired = fired;
fired = 0;
}

for ( i in args ) {
i = args[ i ];
type = jQuery.type( i );
if ( type === "array" ) {
this.then.apply( this , i );
} else if ( type === "function" ) {
callbacks.push( i );
}
}

if ( _fired ) {
deferred.fire( _fired[ 0 ] , _fired[ 1 ] );
}
}
return this;
},

// resolve with given context and args
// (i is used internally)
fire: function( context , args , i ) {
if ( ! cancelled && ! fired && ! firing ) {
firing = 1;
try {
for( i = 0 ; ! cancelled && callbacks[ i ] ; i++ ) {
cancelled = ( callbacks[ i ].apply( context , args ) === false ) && cancellable;
}
} catch( e ) {
cancelled = cancellable;
jQuery.error( e );
} finally {
fired = [ context , args ];
callbacks = cancelled ? [] : callbacks.slice( i + 1 );
firing = 0;
}
}
return this;
},

// resolve with this as context and given arguments
resolve: function() {
deferred.fire( this , arguments );
return this;
},

// cancelling further callbacks
cancel: function() {
if ( cancellable ) {
callbacks = [];
cancelled = 1;
}
return this;
}

};

// Add the deferred marker
deferred.then._ = deferredMarker;

return deferred;
},

// Full fledged deferred (two callbacks list)
// Typical success/error system
deferred: function( func , cancellable ) {

// Handle varargs
if ( arguments.length === 1 ) {

if ( typeof func === "boolean" ) {
cancellable = func;
func = 0;
}
}

var errorDeferred = jQuery._deferred( cancellable ),
deferred = jQuery._deferred( cancellable ),
// Keep reference of the cancel method since we'll redefine it
cancelThen = deferred.cancel;

// Add errorDeferred methods and redefine cancel
jQuery.extend( deferred , {

fail: errorDeferred.then,
fireReject: errorDeferred.fire,
reject: errorDeferred.resolve,
cancel: function() {
cancelThen();
errorDeferred.cancel();
return this;
}

} );

// Make sure only one callback list will be used
deferred.then( errorDeferred.cancel ).fail( cancelThen );

// Call given func if any
if ( func ) {
func.call( deferred , deferred );
}

return deferred;
},

// Check if an object is a deferred
isDeferred: function( object , method ) {
method = method || "then";
return !!( object && object[ method ] && object[ method ]._ === deferredMarker );
},

// Deferred helper
when: function( object , method ) {
method = method || "then";
object = jQuery.isDeferred( object , method ) ?
object :
jQuery.deferred().resolve( object );
object.fail = object.fail || function() { return this; };
object[ method ] = object[ method ] || object.then;
object.then = object.then || object[ method ];
return object;
},

// Use of jQuery.browser is frowned upon.
// More details: http://docs.jquery.com/Utilities/jQuery.browser
@@ -818,6 +953,11 @@ jQuery.extend({
browser: {}
});

// Create readyList deferred
// also force $.fn.ready to be recognized as a defer
readyList = jQuery._deferred( false );
jQuery.fn.ready._ = deferredMarker;

// Populate the class2type map
jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) {
class2type[ "[object " + name + "]" ] = name.toLowerCase();

0 comments on commit 5bacb53

Please sign in to comment.