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

This commit is contained in:
jaubourg 2010-12-20 19:09:15 +01:00 committed by jaubourg
parent c1625f6b79
commit 5bacb53866
3 changed files with 458 additions and 187 deletions

View file

@ -60,8 +60,8 @@ var jQuery = function( selector, context ) {
// Has the ready events already been bound? // Has the ready events already been bound?
readyBound = false, readyBound = false,
// The functions to execute on DOM ready // The deferred used on DOM ready
readyList = [], readyList,
// The ready event handler // The ready event handler
DOMContentLoaded, DOMContentLoaded,
@ -75,7 +75,10 @@ var jQuery = function( selector, context ) {
indexOf = Array.prototype.indexOf, indexOf = Array.prototype.indexOf,
// [[Class]] -> type pairs // [[Class]] -> type pairs
class2type = {}; class2type = {},
// Marker for deferred
deferredMarker = [];
jQuery.fn = jQuery.prototype = { jQuery.fn = jQuery.prototype = {
init: function( selector, context ) { init: function( selector, context ) {
@ -253,22 +256,12 @@ jQuery.fn = jQuery.prototype = {
return jQuery.each( this, callback, args ); return jQuery.each( this, callback, args );
}, },
ready: function( fn ) { ready: function() {
// Attach the listeners // Attach the listeners
jQuery.bindReady(); jQuery.bindReady();
// If the DOM is already ready // Change ready & apply
if ( jQuery.isReady ) { return ( jQuery.fn.ready = readyList.then ).apply( this , arguments );
// 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;
}, },
eq: function( i ) { eq: function( i ) {
@ -415,25 +408,13 @@ jQuery.extend({
} }
// If there are functions bound, to execute // If there are functions bound, to execute
if ( readyList ) { readyList.fire( document , [ jQuery ] );
// 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 // Trigger any bound ready events
if ( jQuery.fn.trigger ) { if ( jQuery.fn.trigger ) {
jQuery( document ).trigger( "ready" ).unbind( "ready" ); jQuery( document ).trigger( "ready" ).unbind( "ready" );
} }
} }
}
}, },
bindReady: function() { bindReady: function() {
@ -801,6 +782,160 @@ jQuery.extend({
return (new Date()).getTime(); 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. // Use of jQuery.browser is frowned upon.
// More details: http://docs.jquery.com/Utilities/jQuery.browser // More details: http://docs.jquery.com/Utilities/jQuery.browser
uaMatch: function( ua ) { uaMatch: function( ua ) {
@ -818,6 +953,11 @@ jQuery.extend({
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 // Populate the class2type map
jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) {
class2type[ "[object " + name + "]" ] = name.toLowerCase(); class2type[ "[object " + name + "]" ] = name.toLowerCase();

View file

@ -18,18 +18,19 @@ jQuery.xhr = function( _native ) {
return jQuery.ajaxSettings.xhr(); return jQuery.ajaxSettings.xhr();
} }
function reset(force) { function reset( force ) {
// We only need to reset if we went through the init phase // We only need to reset if we went through the init phase
// (with the exception of object creation) // (with the exception of object creation)
if ( force || internal ) { if ( force || internal ) {
// Reset callbacks lists // Reset callbacks lists
callbacksLists = { deferred = jQuery.deferred();
success: createCBList(), completeDeferred = jQuery._deferred();
error: createCBList(),
complete: createCBList() xhr.success = xhr.then = deferred.then;
}; xhr.error = xhr.fail = deferred.fail;
xhr.complete = completeDeferred.then;
// Reset private variables // Reset private variables
requestHeaders = {}; requestHeaders = {};
@ -155,8 +156,8 @@ jQuery.xhr = function( _native ) {
callbackContext = s.context || s; callbackContext = s.context || s;
globalEventContext = s.context ? jQuery(s.context) : jQuery.event; globalEventContext = s.context ? jQuery(s.context) : jQuery.event;
for ( i in callbacksLists ) { for ( i in { success:1, error:1, complete:1 } ) {
callbacksLists[i].bind(s[i]); xhr[ i ]( s[ i ] );
} }
// Watch for a new set of requests // Watch for a new set of requests
@ -355,10 +356,12 @@ jQuery.xhr = function( _native ) {
// Keep local copies of vars in case callbacks re-use the xhr // Keep local copies of vars in case callbacks re-use the xhr
var _s = s, var _s = s,
_callbacksLists = callbacksLists, _deferred = deferred,
_completeDeferred = completeDeferred,
_callbackContext = callbackContext, _callbackContext = callbackContext,
_globalEventContext = globalEventContext; _globalEventContext = globalEventContext;
// Set state if the xhr hasn't been re-used // Set state if the xhr hasn't been re-used
function _setState( value ) { function _setState( value ) {
if ( xhr.readyState && s === _s ) { if ( xhr.readyState && s === _s ) {
@ -375,18 +378,20 @@ jQuery.xhr = function( _native ) {
// We're done // We're done
_setState( 4 ); _setState( 4 );
// Success // Success/Error
_callbacksLists.success.fire( isSuccess , _callbackContext , success, statusText, xhr); if ( isSuccess ) {
if ( isSuccess && _s.global ) { _deferred.fire( _callbackContext , [ success , statusText , xhr ] );
_globalEventContext.trigger( "ajaxSuccess", [xhr, _s, success] ); } else {
_deferred.fireReject( _callbackContext , [ xhr , statusText , error ] );
} }
// Error
_callbacksLists.error.fire( ! isSuccess , _callbackContext , xhr, statusText, error); if ( _s.global ) {
if ( !isSuccess && _s.global ) { _globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ) , [ xhr , _s , isSuccess ? success : error ] );
_globalEventContext.trigger( "ajaxError", [xhr, _s, error] );
} }
// Complete // Complete
_callbacksLists.complete.fire( 1 , _callbackContext, xhr, statusText); _completeDeferred.fire( _callbackContext, [ xhr , statusText ] );
if ( _s.global ) { if ( _s.global ) {
_globalEventContext.trigger( "ajaxComplete", [xhr, _s] ); _globalEventContext.trigger( "ajaxComplete", [xhr, _s] );
// Handle the global AJAX counter // Handle the global AJAX counter
@ -419,7 +424,8 @@ jQuery.xhr = function( _native ) {
// Callback stuff // Callback stuff
callbackContext, callbackContext,
globalEventContext, globalEventContext,
callbacksLists, deferred,
completeDeferred,
// Headers (they are sent all at once) // Headers (they are sent all at once)
requestHeaders, requestHeaders,
// Response headers // Response headers
@ -596,135 +602,10 @@ jQuery.xhr = function( _native ) {
// Init data (so that we can bind callbacks early // Init data (so that we can bind callbacks early
reset(1); 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 the xhr emulation
return xhr; 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, { jQuery.extend(jQuery.xhr, {
// Add new prefilter // Add new prefilter

View file

@ -904,3 +904,253 @@ test("jQuery.parseJSON", function(){
ok( true, "Test malformed JSON string." ); 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();
});
});