// 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,
indexOf = Array.prototype.indexOf,
// [[Class]] -> type pairs
- class2type = {};
+ class2type = {},
+
+ // Marker for deferred
+ deferredMarker = [];
jQuery.fn = jQuery.prototype = {
init: function( selector, context ) {
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 ) {
}
// 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" );
}
}
},
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 {
+ 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
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();
return jQuery.ajaxSettings.xhr();
}
- function reset(force) {
+ function reset( force ) {
// We only need to reset if we went through the init phase
// (with the exception of object creation)
if ( force || internal ) {
-
+
// Reset callbacks lists
- callbacksLists = {
- success: createCBList(),
- error: createCBList(),
- complete: createCBList()
- };
+ deferred = jQuery.deferred();
+ completeDeferred = jQuery._deferred();
+
+ xhr.success = xhr.then = deferred.then;
+ xhr.error = xhr.fail = deferred.fail;
+ xhr.complete = completeDeferred.then;
// Reset private variables
requestHeaders = {};
callbackContext = s.context || s;
globalEventContext = s.context ? jQuery(s.context) : jQuery.event;
- for ( i in callbacksLists ) {
- callbacksLists[i].bind(s[i]);
+ for ( i in { success:1, error:1, complete:1 } ) {
+ xhr[ i ]( s[ i ] );
}
// Watch for a new set of requests
// Keep local copies of vars in case callbacks re-use the xhr
var _s = s,
- _callbacksLists = callbacksLists,
+ _deferred = deferred,
+ _completeDeferred = completeDeferred,
_callbackContext = callbackContext,
_globalEventContext = globalEventContext;
+
// Set state if the xhr hasn't been re-used
function _setState( value ) {
if ( xhr.readyState && s === _s ) {
// We're done
_setState( 4 );
- // Success
- _callbacksLists.success.fire( isSuccess , _callbackContext , success, statusText, xhr);
- if ( isSuccess && _s.global ) {
- _globalEventContext.trigger( "ajaxSuccess", [xhr, _s, success] );
+ // Success/Error
+ if ( isSuccess ) {
+ _deferred.fire( _callbackContext , [ success , statusText , xhr ] );
+ } else {
+ _deferred.fireReject( _callbackContext , [ xhr , statusText , error ] );
}
- // Error
- _callbacksLists.error.fire( ! isSuccess , _callbackContext , xhr, statusText, error);
- if ( !isSuccess && _s.global ) {
- _globalEventContext.trigger( "ajaxError", [xhr, _s, error] );
+
+ if ( _s.global ) {
+ _globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ) , [ xhr , _s , isSuccess ? success : error ] );
}
+
// Complete
- _callbacksLists.complete.fire( 1 , _callbackContext, xhr, statusText);
+ _completeDeferred.fire( _callbackContext, [ xhr , statusText ] );
+
if ( _s.global ) {
_globalEventContext.trigger( "ajaxComplete", [xhr, _s] );
// Handle the global AJAX counter
// Callback stuff
callbackContext,
globalEventContext,
- callbacksLists,
+ deferred,
+ completeDeferred,
// Headers (they are sent all at once)
requestHeaders,
// Response headers
// Init data (so that we can bind callbacks early
reset(1);
- // Install callbacks related methods
- jQuery.each(callbacksLists, function(name) {
- var list;
- xhr[name] = function() {
- list = callbacksLists[name];
- if ( list ) {
- list.bind.apply(list, arguments );
- }
- return this;
- };
- });
-
// Return the xhr emulation
return xhr;
};
-// Create a callback list
-function createCBList() {
-
- var functors = [],
- autoFire = 0,
- fireArgs,
- list = {
-
- fire: function( flag , context ) {
-
- // Save info for later bindings
- fireArgs = arguments;
-
- // Remove autoFire to keep bindings in order
- autoFire = 0;
-
- var args = sliceFunc.call( fireArgs , 2 );
-
- // Execute callbacks
- while ( flag && functors.length ) {
- flag = functors.shift().apply( context , args ) !== false;
- }
-
- // Clean if asked to stop
- if ( ! flag ) {
- clean();
- }
-
- // Set autoFire
- autoFire = 1;
- },
-
- bind: function() {
-
- var args = arguments,
- i = 0,
- length = args.length,
- func;
-
- for ( ; i < length ; i++ ) {
-
- func = args[ i ];
-
- if ( jQuery.isArray(func) ) {
-
- list.bind.apply( list , func );
-
- } else if ( isFunction(func) ) {
-
- // Add if not already in
- if ( ! pos( func ) ) {
- functors.push( func );
- }
- }
- }
-
- if ( autoFire ) {
- list.fire.apply( list , fireArgs );
- }
- },
-
- unbind: function() {
-
- var i = 0,
- args = arguments,
- length = args.length,
- func,
- position;
-
- if ( length ) {
-
- for( ; i < length ; i++ ) {
- func = args[i];
- if ( jQuery.isArray(func) ) {
- list.unbind.apply(list,func);
- } else if ( isFunction(func) ) {
- position = pos(func);
- if ( position ) {
- functors.splice(position-1,1);
- }
- }
- }
-
- } else {
-
- functors = [];
-
- }
-
- }
-
- };
-
- // Get the index of the functor in the list (1-based)
- function pos( func ) {
- for (var i = 0, length = functors.length; i < length && functors[i] !== func; i++) {
- }
- return i < length ? ( i + 1 ) : 0;
- }
-
- // Clean the object
- function clean() {
- // Empty callbacks list
- functors = [];
- // Inhibit methods
- for (var i in list) {
- list[i] = jQuery.noop;
- }
- }
-
- return list;
-}
-
jQuery.extend(jQuery.xhr, {
// Add new prefilter
ok( true, "Test malformed JSON string." );
}
});
+
+test("jQuery._deferred()", function() {
+
+ expect( 14 );
+
+ var deferred,
+ object,
+ test;
+
+ deferred = jQuery._deferred();
+
+ test = false;
+
+ deferred.then( function( value ) {
+ equals( value , "value" , "Test pre-resolve callback" );
+ test = true;
+ } );
+
+ deferred.resolve( "value" );
+
+ ok( test , "Test pre-resolve callbacks called right away" );
+
+ test = false;
+
+ deferred.then( function( value ) {
+ equals( value , "value" , "Test post-resolve callback" );
+ test = true;
+ } );
+
+ ok( test , "Test post-resolve callbacks called right away" );
+
+ deferred.cancel();
+
+ test = true;
+
+ deferred.then( function() {
+ ok( false , "Manual cancel was ignored" );
+ test = false;
+ } );
+
+ ok( test , "Test manual cancel" );
+
+ deferred = jQuery._deferred().then( function() {
+ return false;
+ } );
+
+ deferred.resolve();
+
+ test = true;
+
+ deferred.then( function() {
+ test = false;
+ } );
+
+ ok( test , "Test cancel by returning false" );
+
+ try {
+ deferred = jQuery._deferred().resolve().then( function() {
+ throw "Error";
+ } , function() {
+ ok( false , "Test deferred cancel on exception" );
+ } );
+ } catch( e ) {
+ strictEqual( e , "Error" , "Test deferred propagates exceptions");
+ deferred.then();
+ }
+
+ test = "";
+ deferred = jQuery._deferred().then( function() {
+
+ test += "A";
+
+ }, function() {
+
+ test += "B";
+
+ } ).resolve();
+
+ strictEqual( test , "AB" , "Test multiple then parameters" );
+
+ test = "";
+
+ deferred.then( function() {
+
+ deferred.then( function() {
+
+ test += "C";
+
+ } );
+
+ test += "A";
+
+ }, function() {
+
+ test += "B";
+ } );
+
+ strictEqual( test , "ABC" , "Test then callbacks order" );
+
+ deferred = jQuery._deferred( false ).resolve().cancel();
+
+ deferred.then( function() {
+ ok( true , "Test non-cancellable deferred not cancelled manually");
+ return false;
+ } );
+
+ deferred.then( function() {
+ ok( true , "Test non-cancellable deferred not cancelled by returning false");
+ } );
+
+ try {
+ deferred.then( function() {
+ throw "Error";
+ } , function() {
+ ok( true , "Test non-cancellable deferred keeps callbacks after exception" );
+ } );
+ } catch( e ) {
+ strictEqual( e , "Error" , "Test non-cancellable deferred propagates exceptions");
+ deferred.then();
+ }
+
+ deferred = jQuery._deferred();
+
+ deferred.fire( jQuery , [ document ] ).then( function( doc ) {
+ ok( this === jQuery && arguments.length === 1 && doc === document , "Test fire context & args" );
+ });
+});
+
+test("jQuery.deferred()", function() {
+
+ expect( 8 );
+
+ jQuery.deferred( function( defer ) {
+ strictEqual( this , defer , "Defer passed as this & first argument" );
+ this.resolve( "done" );
+ }).then( function( value ) {
+ strictEqual( value , "done" , "Passed function executed" );
+ });
+
+ jQuery.deferred().resolve().then( function() {
+ ok( true , "Success on resolve" );
+ }).fail( function() {
+ ok( false , "Error on resolve" );
+ });
+
+ jQuery.deferred().reject().then( function() {
+ ok( false , "Success on reject" );
+ }).fail( function() {
+ ok( true , "Error on reject" );
+ });
+
+ var flag = true;
+
+ jQuery.deferred().resolve().cancel().then( function() {
+ ok( flag = false , "Success on resolve/cancel" );
+ }).fail( function() {
+ ok( flag = false , "Error on resolve/cancel" );
+ });
+
+ ok( flag , "Cancel on resolve" );
+
+ flag = true;
+
+ jQuery.deferred().reject().cancel().then( function() {
+ ok( flag = false , "Success on reject/cancel" );
+ }).fail( function() {
+ ok( flag = false , "Error on reject/cancel" );
+ });
+
+ ok( flag , "Cancel on reject" );
+
+ jQuery.deferred( false ).resolve().then( function() {
+ return false;
+ } , function() {
+ ok( true , "Not cancelled on resolve" );
+ });
+
+ jQuery.deferred( false ).reject().fail( function() {
+ return false;
+ } , function() {
+ ok( true , "Not cancelled on reject" );
+ });
+
+});
+
+test("jQuery.isDeferred()", function() {
+
+ expect( 11 );
+
+ var object1 = { then: function() { return this; } },
+ object2 = { then: function() { return this; } };
+
+ object2.then._ = [];
+
+ // The use case that we want to match
+ ok(jQuery.isDeferred(jQuery._deferred()), "Simple deferred");
+ ok(jQuery.isDeferred(jQuery.deferred()), "Failable deferred");
+
+ // Some other objects
+ ok(!jQuery.isDeferred(object1), "Object with then & no marker");
+ ok(!jQuery.isDeferred(object2), "Object with then & marker");
+
+ // Not objects shouldn't be matched
+ ok(!jQuery.isDeferred(""), "string");
+ ok(!jQuery.isDeferred(0) && !jQuery.isDeferred(1), "number");
+ ok(!jQuery.isDeferred(true) && !jQuery.isDeferred(false), "boolean");
+ ok(!jQuery.isDeferred(null), "null");
+ ok(!jQuery.isDeferred(undefined), "undefined");
+
+ object1 = {custom: jQuery._deferred().then};
+
+ ok(!jQuery.isDeferred(object1) , "custom method name not found automagically");
+ ok(jQuery.isDeferred(object1,"custom") , "custom method name");
+});
+
+test("jQuery.when()", function() {
+
+ expect( 5 );
+
+ var cache, i, deferred = { done: jQuery.deferred().resolve( 1 ).then };
+
+ for( i = 1 ; i < 3 ; i++ ) {
+ jQuery.when( cache || jQuery.deferred( function() {
+ this.resolve( i );
+ }) ).then( function( value ) {
+ strictEqual( value , 1 , "Function executed" + ( i > 1 ? " only once" : "" ) );
+ cache = value;
+ }).fail( function() {
+ ok( false , "Fail called" );
+ });
+ }
+
+ cache = 0;
+
+ for( i = 1 ; i < 3 ; i++ ) {
+ jQuery.when( cache || deferred , "done" ).done( function( value ) {
+ strictEqual( value , 1 , "Custom method: resolved" + ( i > 1 ? " only once" : "" ) );
+ cache = value;
+ }).fail( function() {
+ ok( false , "Custom method: fail called" );
+ });
+ }
+
+ stop();
+
+ jQuery.when( jQuery( document ) , "ready" ).then( function( test ) {
+ strictEqual( test , jQuery , "jQuery.fn.ready recognized as a deferred" );
+ start();
+ });
+});