Permalink
Browse files

Added deferred to core. Used internally for DOM readyness and ajax ca…

…llbacks.
  • Loading branch information...
1 parent 1f92ede commit 116c82b027a03a7a5670fa580fa9af819cc1cc03 jaubourg committed with unknown Dec 20, 2010
Showing with 450 additions and 179 deletions.
  1. +174 −34 src/core.js
  2. +26 −145 src/xhr.js
  3. +250 −0 test/unit/core.js
View
@@ -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 {
@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.

@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.

@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.

@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.

@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 :/

@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.

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

@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.

@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();
Oops, something went wrong.

3 comments on commit 116c82b

@rmurphey

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

@jaubourg
Member

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

Meh. I was afraid you'd say that.

Please sign in to comment.