From 4621a0131b98461e41082aa0aaf73f9c6f4ca9ce Mon Sep 17 00:00:00 2001 From: Corey Frang Date: Tue, 22 May 2012 23:04:45 -0400 Subject: [PATCH] Optimizations to animation queue/promise logic, closes gh-776. --- src/effects.js | 69 ++++++++++++++--------- src/queue.js | 125 +++++++++++++++-------------------------- test/unit/effects.js | 81 +++++++++++++++++++++++++++ test/unit/queue.js | 129 ++++++++++++++++++------------------------- 4 files changed, 224 insertions(+), 180 deletions(-) diff --git a/src/effects.js b/src/effects.js index 41e87e3af..c213e69d2 100644 --- a/src/effects.js +++ b/src/effects.js @@ -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 ); } }; diff --git a/src/queue.js b/src/queue.js index 907baf4d6..175603781 100644 --- a/src/queue.js +++ b/src/queue.js @@ -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(); diff --git a/test/unit/effects.js b/test/unit/effects.js index fed25eeb0..a2645b106 100644 --- a/test/unit/effects.js +++ b/test/unit/effects.js @@ -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 diff --git a/test/unit/queue.js b/test/unit/queue.js index 6365df190..6a614edb6 100644 --- a/test/unit/queue.js +++ b/test/unit/queue.js @@ -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 ); +}); -- 2.39.5