Added deferred to core. Used internally for DOM readyness and ajax callbacks.
authorjaubourg <aubourg.julian@gmail.com>
Mon, 20 Dec 2010 18:09:15 +0000 (19:09 +0100)
committerunknown <Julian@.(none)>
Fri, 24 Dec 2010 11:55:46 +0000 (12:55 +0100)
src/core.js
src/xhr.js
test/unit/core.js

index 346e52d..f7713ca 100644 (file)
@@ -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 {
+                                                       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();
index 4896e6c..8e9a2b0 100644 (file)
@@ -18,18 +18,19 @@ jQuery.xhr = function( _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 = {};
@@ -155,8 +156,8 @@ 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 ) {
@@ -375,18 +378,20 @@ 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
index 7057783..f411724 100644 (file)
@@ -902,3 +902,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();
+       });
+});