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 unknown committed Dec 24, 2010
1 parent 1f92ede commit 116c82b
Show file tree
Hide file tree
Showing 3 changed files with 450 additions and 179 deletions.
@@ -60,8 +60,8 @@ 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 ) {
@@ -253,22 +256,12 @@ jQuery.fn = jQuery.prototype = {
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() {

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 {

This comment has been minimized.

Copy link
@rmurphey

rmurphey Dec 24, 2010

If I'm reading this right, if a callback throws an error then no subsequent callbacks will run; likewise if a callback returns false. I'm not sure this is ideal -- if one piece of code is broken, should it break other code? Should one piece of code have the ability to unilaterally prevent other code from reacting to the resolution of the dfd? In that scenario, suddenly the order in which stuff is bound becomes a big deal; not sure that's a good thing.

This comment has been minimized.

Copy link
@dmethvin

dmethvin Dec 24, 2010

Member

The pre-Deferreds jQuery doesn't have try/catch around the ready or ajax-events so when there are multiples attached we just plain die on the first error. The benefit of that is that the browser can generally give a specific line number in the user's code where the problem occurred. If it's wrapped in a try/catch as here and then re-thrown via jQuery.error, it's gonna be difficult to debug. Yes we can continue, but for several browsers (I know IE offhand) there is no line information in the Error object (and dammit, ES5 punted on this issue). So the user just gets a message like "null is null or not an object" thown out of a stack of anonymous functions and can't figure out which line it's in.

TL;DR: All the choices suck. For code I've done in the past I've gone as far as having a debug mode that avoids the try/catch so I could get debug info.

This comment has been minimized.

Copy link
@jaubourg

jaubourg Dec 24, 2010

Member

@rmurphey:

  • for cancellable deferreds (default): an exception will effectively cancel the deferred and be rethrown.
  • for non-cancellable deferreds: an exception will stop the loop and be rethrown BUT you can still re-start the loop starting at the next callback by calling the then method with no argument.

You have two situations when you are confronted to this situation:

  1. when you resolve the deferred, in which case you can try/catch and loop until everything has been called (for non-cancellable deferreds that is)
  2. when you attach a callback using then and the deferred was already resolved in which case you can try/catch here too.

As for the possibility to cancel the deferred, I still think it can be useful in some instances. Maybe it would be better to have deferred non-cancellable by default?

@dmethvin:

Debugging with rethrow is such a nightmare. A finally block is enough for IE to lose track of everything, even if you don't have a catch block and do not rethrow the exception. There are half solutions around this, but they involve at least a secondary array and quite a tricky logic in "then" in order to keep proper callback order. Like you say, all the choices suck.

On a side note, $.ready is implemented with a non-cancellable deferred which means, while the exception is rethrown, you're still able to get the following ready blocks called.

This comment has been minimized.

Copy link
@rmurphey

rmurphey Dec 24, 2010

@jaubourg

I think what you're saying is that, by default, one callback can put a stop to the whole resolution chain. I'm not sure this makes sense, because that means the order in which the callbacks were attached becomes important, and that seems to violate the idea of a promise. In my mind, the idea of a promise is that every callback that registers an interest can expect to have access to its outcome. I don't think other callbacks should be able to modify that outcome -- aka, the "promise." At the very least, I think this behavior shouldn't be the default.

This comment has been minimized.

Copy link
@jaubourg

jaubourg Dec 24, 2010

Member

@rmurphey:

Are you suggesting exceptions should be swallowed silently? I agree with the overall design goals of a promise but exceptions have to be thrown somehow or debugging using deferred would be close to impossible. Non-cancellable by default makes sense but I'm afraid it is not practical to go any further :/

This comment has been minimized.

Copy link
@rmurphey

rmurphey Dec 24, 2010

@jaubourg

Thrown errors are one thing, and no, I definitely don't want to see those get swallowed silently :)

I think my concern is more with the general ability for a callback to return false and, if I'm reading this correctly, stop the remainder of the callback chain if the deferred is cancellable. This behavior might be valuable optionally, but I don't think it should be the default. My two cents.

This comment has been minimized.

Copy link
@jaubourg

jaubourg Dec 24, 2010

Member

@rmurphey:

ok, I think we are on the same page here :)

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 ];

This comment has been minimized.

Copy link
@rmurphey

rmurphey Dec 24, 2010

Seems it could be valuable to expose fired as a property of the Deferred instance; maybe also set the results and the cancelled-ness of the Deferred as properties as well? I've found these properties useful for testing purposes, at least, with Dojo's deferreds.

This comment has been minimized.

Copy link
@jaubourg

jaubourg Dec 24, 2010

Member

I'd much prefer methods actually since it is so easy for third party to mess with exposed properties (and render them unusable to any code trying to use them afterward). Also, I wouldn't like to have the context and arguments accessible not only for the very reason I just talked about but also because it is an invitation to actively wait for them (ie. some setTimeout or setInterval madness).

What I'd envision would be:

  • isResolved(): boolean
  • isRejected(): boolean
  • isCancelled(): boolean

If you get those three, it's easy enough to call "then" or "fail" accordingly to get the context and arguments actually stored in fired.

This comment has been minimized.

Copy link
@rmurphey

rmurphey Dec 24, 2010

Methods are fine too -- something to capture that state info would be great either way :)

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();

3 comments on commit 116c82b

@rmurphey
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an overarching issue: IMVHO the new Deferred stuff probably belongs in deferred.js, not core.js.

@jaubourg
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If $.ready() makes use of them then they have to be in core. I had them in a separate file at first but I quite quickly moved the code in core.

@rmurphey
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meh. I was afraid you'd say that.

Please sign in to comment.