From 5bacb53866dbc3fbb36202a25c756a4ea2fd5965 Mon Sep 17 00:00:00 2001 From: jaubourg Date: Mon, 20 Dec 2010 19:09:15 +0100 Subject: [PATCH] Added deferred to core. Used internally for DOM readyness and ajax callbacks. --- src/core.js | 212 ++++++++++++++++++++++++++++++++------- src/xhr.js | 183 ++++++--------------------------- test/unit/core.js | 250 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 458 insertions(+), 187 deletions(-) diff --git a/src/core.js b/src/core.js index e463ade0..d41b9a9b 100644 --- a/src/core.js +++ b/src/core.js @@ -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() { + + 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(); diff --git a/src/xhr.js b/src/xhr.js index c7153963..51bd3dfd 100644 --- a/src/xhr.js +++ b/src/xhr.js @@ -17,20 +17,21 @@ jQuery.xhr = function( _native ) { if ( _native ) { 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 = {}; responseHeadersString = responseHeaders = internal = done = timeoutTimer = s = undefined; @@ -154,9 +155,9 @@ jQuery.xhr = function( _native ) { 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 @@ -355,10 +356,12 @@ jQuery.xhr = function( _native ) { // 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 ) { @@ -374,19 +377,21 @@ jQuery.xhr = function( _native ) { // 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 @@ -419,7 +424,8 @@ jQuery.xhr = function( _native ) { // Callback stuff callbackContext, globalEventContext, - callbacksLists, + deferred, + completeDeferred, // Headers (they are sent all at once) requestHeaders, // Response headers @@ -596,135 +602,10 @@ jQuery.xhr = function( _native ) { // 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 diff --git a/test/unit/core.js b/test/unit/core.js index 5c80569c..e847de1d 100644 --- a/test/unit/core.js +++ b/test/unit/core.js @@ -904,3 +904,253 @@ test("jQuery.parseJSON", function(){ 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(); + }); +});