]> source.dussan.org Git - jquery.git/commitdiff
Optimizations to animation queue/promise logic, closes gh-776.
authorCorey Frang <gnarf@gnarf.net>
Wed, 23 May 2012 03:04:45 +0000 (23:04 -0400)
committerDave Methvin <dave.methvin@gmail.com>
Wed, 23 May 2012 03:04:45 +0000 (23:04 -0400)
src/effects.js
src/queue.js
test/unit/effects.js
test/unit/queue.js

index 41e87e3af7d37c25cd89637658466109c7eb38fc..c213e69d2356bb9b66adf9cbf72939c55d3cf5a0 100644 (file)
@@ -4,7 +4,7 @@ var fxNow, timerId, iframe, iframeDoc,
        elemdisplay = {},
        rfxtypes = /^(?:toggle|show|hide)$/,
        rfxnum = /^([\-+]=)?((?:\d*\.)?\d+)([a-z%]*)$/i,
-       rrun = /\.run$/,
+       rrun = /queueHooks$/,
        animationPrefilters = [ defaultPrefilter ],
        tweeners = {
                "*": [function( prop, value ) {
@@ -212,12 +212,34 @@ jQuery.Animation = jQuery.extend( Animation, {
 });
 
 function defaultPrefilter( elem, props, opts ) {
-       var index, prop, value, length, dataShow, tween,
+       var index, prop, value, length, dataShow, tween, hooks, oldfire,
+               anim = this,
                style = elem.style,
                orig = {},
                handled = [],
                hidden = elem.nodeType && isHidden( elem );
 
+       // handle queue: false promises
+       if ( !opts.queue ) {
+               hooks = jQuery._queueHooks( elem, "fx" );
+               if ( hooks.unqueued == null ) {
+                       hooks.unqueued = 0;
+                       oldfire = hooks.empty.fire;
+                       hooks.empty.fire = function() {
+                               if ( !hooks.unqueued ) {
+                                       oldfire();
+                               }
+                       };
+               }
+               hooks.unqueued++;
+               anim.always(function() {
+                       hooks.unqueued--;
+                       if ( !jQuery.queue( elem, "fx" ).length ) {
+                               hooks.empty.fire();
+                       }
+               });
+       }
+
        // height/width overflow pass
        if ( elem.nodeType === 1 && ( props.height || props.width ) ) {
                // Make sure that nothing sneaks out
@@ -244,7 +266,7 @@ function defaultPrefilter( elem, props, opts ) {
 
        if ( opts.overflow ) {
                style.overflow = "hidden";
-               this.finish(function() {
+               anim.finish(function() {
                        style.overflow = opts.overflow[ 0 ];
                        style.overflowX = opts.overflow[ 1 ];
                        style.overflowY = opts.overflow[ 2 ];
@@ -270,11 +292,11 @@ function defaultPrefilter( elem, props, opts ) {
                if ( hidden ) {
                        showHide([ elem ], true );
                } else {
-                       this.finish(function() {
+                       anim.finish(function() {
                                showHide([ elem ]);
                        });
                }
-               this.finish(function() {
+               anim.finish(function() {
                        var prop;
                        jQuery.removeData( elem, "fxshow", true );
                        for ( prop in orig ) {
@@ -283,7 +305,7 @@ function defaultPrefilter( elem, props, opts ) {
                });
                for ( index = 0 ; index < length ; index++ ) {
                        prop = handled[ index ];
-                       tween = this.createTween( prop, hidden ? dataShow[ prop ] : 0 );
+                       tween = anim.createTween( prop, hidden ? dataShow[ prop ] : 0 );
                        orig[ prop ] = dataShow[ prop ] || jQuery.style( elem, prop );
 
                        if ( !( prop in dataShow ) ) {
@@ -482,10 +504,10 @@ jQuery.fn.extend({
                        this.queue( optall.queue, doAnimation );
        },
        stop: function( type, clearQueue, gotoEnd ) {
-               var stopQueue = function( elem, data, index ) {
-                       var hooks = data[ index ];
-                       jQuery.removeData( elem, index, true );
-                       hooks.stop( gotoEnd );
+               var stopQueue = function( hooks ) {
+                       var stop = hooks.stop;
+                       delete hooks.stop;
+                       stop( gotoEnd );
                };
 
                if ( typeof type !== "string" ) {
@@ -498,30 +520,27 @@ jQuery.fn.extend({
                }
 
                return this.each(function() {
-                       var index,
-                               hadTimers = false,
+                       var dequeue = true,
+                               index = type != null && type + "queueHooks",
                                timers = jQuery.timers,
                                data = jQuery._data( this );
 
-                       // clear marker counters if we know they won't be
-                       if ( !gotoEnd ) {
-                               jQuery._unmark( true, this );
-                       }
-
-                       if ( type == null ) {
+                       if ( index ) {
+                               if ( data[ index ] && data[ index ].stop ) {
+                                       stopQueue( data[ index ] );
+                               }
+                       } else {
                                for ( index in data ) {
                                        if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
-                                               stopQueue( this, data, index );
+                                               stopQueue( data[ index ] );
                                        }
                                }
-                       } else if ( data[ index = type + ".run" ] && data[ index ].stop ){
-                               stopQueue( this, data, index );
                        }
 
                        for ( index = timers.length; index--; ) {
                                if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {
                                        timers[ index ].anim.stop( gotoEnd );
-                                       hadTimers = true;
+                                       dequeue = false;
                                        timers.splice( index, 1 );
                                }
                        }
@@ -529,7 +548,7 @@ jQuery.fn.extend({
                        // start the next in the queue if the last step wasn't forced
                        // timers currently will call their complete callbacks, which will dequeue
                        // but only if they were gotoEnd
-                       if ( !( gotoEnd && hadTimers ) ) {
+                       if ( dequeue || !gotoEnd ) {
                                jQuery.dequeue( this, type );
                        }
                });
@@ -589,15 +608,13 @@ jQuery.speed = function( speed, easing, fn ) {
        // Queueing
        opt.old = opt.complete;
 
-       opt.complete = function( noUnmark ) {
+       opt.complete = function() {
                if ( jQuery.isFunction( opt.old ) ) {
                        opt.old.call( this );
                }
 
                if ( opt.queue ) {
                        jQuery.dequeue( this, opt.queue );
-               } else if ( noUnmark !== false ) {
-                       jQuery._unmark( this );
                }
        };
 
index 907baf4d6ea7a5c4542a61149e162e60d40dc905..17560378120511aab79a04743489fa08b4135d34 100644 (file)
@@ -1,68 +1,22 @@
 (function( jQuery ) {
 
-function handleQueueMarkDefer( elem, type, src ) {
-       var deferDataKey = type + "defer",
-               queueDataKey = type + "queue",
-               markDataKey = type + "mark",
-               defer = jQuery._data( elem, deferDataKey );
-       if ( defer &&
-               ( src === "queue" || !jQuery._data(elem, queueDataKey) ) &&
-               ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) {
-               // Give room for hard-coded callbacks to fire first
-               // and eventually mark/queue something else on the element
-               setTimeout( function() {
-                       if ( !jQuery._data( elem, queueDataKey ) &&
-                               !jQuery._data( elem, markDataKey ) ) {
-                               jQuery.removeData( elem, deferDataKey, true );
-                               defer.fire();
-                       }
-               }, 0 );
-       }
-}
-
 jQuery.extend({
-
-       _mark: function( elem, type ) {
-               if ( elem ) {
-                       type = ( type || "fx" ) + "mark";
-                       jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 );
-               }
-       },
-
-       _unmark: function( force, elem, type ) {
-               if ( force !== true ) {
-                       type = elem;
-                       elem = force;
-                       force = false;
-               }
-               if ( elem ) {
-                       type = type || "fx";
-                       var key = type + "mark",
-                               count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 );
-                       if ( count ) {
-                               jQuery._data( elem, key, count );
-                       } else {
-                               jQuery.removeData( elem, key, true );
-                               handleQueueMarkDefer( elem, type, "mark" );
-                       }
-               }
-       },
-
        queue: function( elem, type, data ) {
-               var q;
+               var queue;
+
                if ( elem ) {
                        type = ( type || "fx" ) + "queue";
-                       q = jQuery._data( elem, type );
+                       queue = jQuery._data( elem, type );
 
                        // Speed up dequeue by getting out quickly if this is just a lookup
                        if ( data ) {
-                               if ( !q || jQuery.isArray(data) ) {
-                                       q = jQuery._data( elem, type, jQuery.makeArray(data) );
+                               if ( !queue || jQuery.isArray(data) ) {
+                                       queue = jQuery._data( elem, type, jQuery.makeArray(data) );
                                } else {
-                                       q.push( data );
+                                       queue.push( data );
                                }
                        }
-                       return q || [];
+                       return queue || [];
                }
        },
 
@@ -71,7 +25,10 @@ jQuery.extend({
 
                var queue = jQuery.queue( elem, type ),
                        fn = queue.shift(),
-                       hooks = {};
+                       hooks = jQuery._queueHooks( elem, type ),
+                       next = function() {
+                               jQuery.dequeue( elem, type );
+                       };
 
                // If the fx queue is dequeued, always remove the progress sentinel
                if ( fn === "inprogress" ) {
@@ -79,22 +36,31 @@ jQuery.extend({
                }
 
                if ( fn ) {
+
                        // Add a progress sentinel to prevent the fx queue from being
                        // automatically dequeued
                        if ( type === "fx" ) {
                                queue.unshift( "inprogress" );
                        }
 
-                       jQuery._data( elem, type + ".run", hooks );
-                       fn.call( elem, function() {
-                               jQuery.dequeue( elem, type );
-                       }, hooks );
+                       // clear up the last queue stop function
+                       delete hooks.stop;
+                       fn.call( elem, next, hooks );
                }
-
-               if ( !queue.length ) {
-                       jQuery.removeData( elem, type + "queue " + type + ".run", true );
-                       handleQueueMarkDefer( elem, type, "queue" );
+               if ( !queue.length && hooks ) {
+                       hooks.empty.fire();
                }
+       },
+
+       // not intended for public consumption - generates a queueHooks object, or returns the current one
+       _queueHooks: function( elem, type ) {
+               var key = type + "queueHooks";
+               return jQuery._data( elem, key ) || jQuery._data( elem, key, {
+                       empty: jQuery.Callbacks("once memory").add(function() {
+                               jQuery.removeData( elem, type + "queue", true );
+                               jQuery.removeData( elem, key, true );
+                       })
+               });
        }
 });
 
@@ -117,6 +83,9 @@ jQuery.fn.extend({
                        this.each(function() {
                                var queue = jQuery.queue( this, type, data );
 
+                               // ensure a hooks for this queue
+                               jQuery._queueHooks( this, type );
+
                                if ( type === "fx" && queue[0] !== "inprogress" ) {
                                        jQuery.dequeue( this, type );
                                }
@@ -146,31 +115,27 @@ jQuery.fn.extend({
        // Get a promise resolved when queues of a certain type
        // are emptied (fx is the type by default)
        promise: function( type, object ) {
+               var tmp,
+                       count = 1,
+                       defer = jQuery.Deferred(),
+                       elements = this,
+                       i = this.length,
+                       resolve = function() {
+                               if ( !( --count ) ) {
+                                       defer.resolveWith( elements, [ elements ] );
+                               }
+                       };
+
                if ( typeof type !== "string" ) {
                        object = type;
                        type = undefined;
                }
                type = type || "fx";
-               var defer = jQuery.Deferred(),
-                       elements = this,
-                       i = elements.length,
-                       count = 1,
-                       deferDataKey = type + "defer",
-                       queueDataKey = type + "queue",
-                       markDataKey = type + "mark",
-                       tmp;
-               function resolve() {
-                       if ( !( --count ) ) {
-                               defer.resolveWith( elements, [ elements ] );
-                       }
-               }
+
                while( i-- ) {
-                       if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) ||
-                                       ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) ||
-                                               jQuery.data( elements[ i ], markDataKey, undefined, true ) ) &&
-                                       jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) {
+                       if ( (tmp = jQuery._data( elements[ i ], type + "queueHooks" )) && tmp.empty ) {
                                count++;
-                               tmp.add( resolve );
+                               tmp.empty.add( resolve );
                        }
                }
                resolve();
index fed25eeb0124ab37567a85a15e57057d90d310d1..a2645b10698157b5d7a863019c9ef73ccee5f485 100644 (file)
@@ -1628,3 +1628,84 @@ asyncTest( "hide, fadeOut and slideUp called on element width height and width =
                start();
        });
 });
+
+asyncTest( "Handle queue:false promises", 10, function() {
+       var foo = jQuery( "#foo" ).clone().andSelf(),
+               step = 1;
+
+       foo.animate({
+               top: 1
+       }, {
+               duration: 10,
+               queue: false,
+               complete: function() {
+                       ok( step++ <= 2, "Step one or two" );
+               }
+       }).animate({
+               bottom: 1
+       }, {
+               duration: 10,
+               complete: function() {
+                       ok( step > 2 && step < 5, "Step three or four" );
+                       step++;
+               }
+       });
+
+       foo.promise().done( function() {
+               equal( step++, 5, "steps 1-5: queue:false then queue:fx done" );
+               foo.animate({
+                       top: 10
+               }, {
+                       duration: 10,
+                       complete: function() {
+                               ok( step > 5 && step < 8, "Step six or seven" );
+                               step++;
+                       }
+               }).animate({
+                       bottom: 10
+               }, {
+                       duration: 10,
+                       queue: false,
+                       complete: function() {
+                               ok( step > 7 && step < 10, "Step eight or nine" );
+                               step++;
+                       }
+               }).promise().done( function() {
+                       equal( step++, 10, "steps 6-10: queue:fx then queue:false" );
+                       start();
+               });
+
+       });
+});
+
+asyncTest( "multiple unqueued and promise", 4, function() {
+       var foo = jQuery( "#foo" ),
+               step = 1;
+       foo.animate({
+               marginLeft: 300
+       }, {
+               duration: 500,
+               queue: false,
+               complete: function() {
+                       strictEqual( step++, 2, "Step 2" );
+               }
+       }).animate({
+               top: 100
+       }, {
+               duration: 1500,
+               queue: false,
+               complete: function() {
+                       strictEqual( step++, 3, "Step 3" );
+               }
+       }).animate({}, {
+               duration: 2000,
+               queue: false,
+               complete: function() {
+                       // no properties is a non-op and finishes immediately
+                       strictEqual( step++, 1, "Step 1" );
+               }
+       }).promise().done( function() {
+               strictEqual( step++, 4, "Step 4" );
+               start();
+       });
+});
\ No newline at end of file
index 6365df1903b0c35e0715e00e8c563b439f58bbc3..6a614edb6b03151189bca909f1989b0c6b0d0c2b 100644 (file)
@@ -1,15 +1,15 @@
-module("queue", { teardown: moduleTeardown });
+module( "queue", {
+       teardown: moduleTeardown
+});
 
-test("queue() with other types",function() {
-       expect(12);
+test( "queue() with other types", 12, function() {
        var counter = 0;
 
        stop();
 
        var $div = jQuery({}),
                defer;
-
-       $div.promise("foo").done(function() {
+       $div.promise( "foo" ).done(function() {
                equal( counter, 0, "Deferred for collection with no queue is automatically resolved" );
        });
 
@@ -30,7 +30,7 @@ test("queue() with other types",function() {
                });
 
        defer = $div.promise("foo").done(function() {
-               equal(  counter, 4, "Testing previous call to dequeue in deferred"  );
+               equal( counter, 4, "Testing previous call to dequeue in deferred"  );
                start();
        });
 
@@ -216,85 +216,46 @@ test("clearQueue() clears the fx queue", function() {
        div.removeData();
 });
 
-test("_mark() and _unmark()", function() {
-       expect(1);
-
-       var div = {},
-               $div = jQuery( div );
+asyncTest( "fn.promise() - called when fx queue is empty", 3, function() {
+       var foo = jQuery( "#foo" ).clone().andSelf(),
+               promised = false;
 
-       stop();
-
-       jQuery._mark( div, "foo" );
-       jQuery._mark( div, "foo" );
-       jQuery._unmark( div, "foo" );
-       jQuery._unmark( div, "foo" );
-
-       $div.promise( "foo" ).done(function() {
-               ok( true, "No more marks" );
-               start();
+       foo.queue( function( next ) {
+               // called twice!
+               ok( !promised, "Promised hasn't been called" );
+               setTimeout( next, 10 );
        });
-});
-
-test("_mark() and _unmark() default to 'fx'", function() {
-       expect(1);
-
-       var div = {},
-               $div = jQuery( div );
-
-       stop();
-
-       jQuery._mark( div );
-       jQuery._mark( div );
-       jQuery._unmark( div, "fx" );
-       jQuery._unmark( div );
-
-       $div.promise().done(function() {
-               ok( true, "No more marks" );
+       foo.promise().done( function() {
+               ok( promised = true, "Promised" );
                start();
        });
 });
 
-test("promise()", function() {
-       expect(1);
-
-       stop();
-
-       var objects = [];
-
-       jQuery.each( [{}, {}], function( i, div ) {
-               var $div = jQuery( div );
-               $div.queue(function( next ) {
-                       setTimeout( function() {
-                               if ( i ) {
-                                       next();
-                                       setTimeout( function() {
-                                               jQuery._unmark( div );
-                                       }, 20 );
-                               } else {
-                                       jQuery._unmark( div );
-                                       setTimeout( function() {
-                                               next();
-                                       }, 20 );
-                               }
-                       }, 50 );
-               }).queue(function( next ) {
-                       next();
-               });
-               jQuery._mark( div );
-               objects.push( $div );
+asyncTest( "fn.promise( \"queue\" ) - called whenever last queue function is dequeued", 5, function() {
+       var foo = jQuery( "#foo" ),
+               test;
+       foo.promise( "queue" ).done( function() {
+               strictEqual( test, undefined, "called immediately when queue was already empty" );
        });
-
-       jQuery.when.apply( jQuery, objects ).done(function() {
-               ok( true, "Deferred resolved" );
-               start();
+       test = 1;
+       foo.queue( "queue", function( next ) {
+               strictEqual( test++, 1, "step one" );
+               setTimeout( next, 0 );
+       }).queue( "queue", function( next ) {
+               strictEqual( test++, 2, "step two" );
+               setTimeout( function() {
+                       strictEqual( test++, 4, "step four" );
+                       next();
+                       start();
+               }, 10 );
+       }).promise( "queue" ).done( function() {
+               strictEqual( test++, 3, "step three" );
        });
 
-       jQuery.each( objects, function() {
-               this.dequeue();
-       });
+       foo.dequeue( "queue" );
 });
 
-test(".promise(obj)", function() {
+test( ".promise(obj)", function() {
        expect(2);
 
        var obj = {};
@@ -303,3 +264,23 @@ test(".promise(obj)", function() {
        ok( jQuery.isFunction( promise.promise ), ".promise(type, obj) returns a promise" );
        strictEqual( promise, obj, ".promise(type, obj) returns obj" );
 });
+
+asyncTest( "queue stop hooks", 2, function() {
+       var foo = jQuery( "#foo" );
+
+       foo.queue( function( next, hooks ) {
+               hooks.stop = function( gotoEnd ) {
+                       equal( !!gotoEnd, false, "Stopped without gotoEnd" );
+               };
+       });
+       foo.stop();
+
+       foo.queue( function( next, hooks ) {
+               hooks.stop = function( gotoEnd ) {
+                       equal( gotoEnd, true, "Stopped with gotoEnd" );
+                       start();
+               };
+       });
+
+       foo.stop( false, true );
+});