Commit 4621a013 authored by Corey Frang's avatar Corey Frang Committed by Dave Methvin

Optimizations to animation queue/promise logic, closes gh-776.

parent ae20e732
...@@ -4,7 +4,7 @@ var fxNow, timerId, iframe, iframeDoc, ...@@ -4,7 +4,7 @@ var fxNow, timerId, iframe, iframeDoc,
elemdisplay = {}, elemdisplay = {},
rfxtypes = /^(?:toggle|show|hide)$/, rfxtypes = /^(?:toggle|show|hide)$/,
rfxnum = /^([\-+]=)?((?:\d*\.)?\d+)([a-z%]*)$/i, rfxnum = /^([\-+]=)?((?:\d*\.)?\d+)([a-z%]*)$/i,
rrun = /\.run$/, rrun = /queueHooks$/,
animationPrefilters = [ defaultPrefilter ], animationPrefilters = [ defaultPrefilter ],
tweeners = { tweeners = {
"*": [function( prop, value ) { "*": [function( prop, value ) {
...@@ -212,12 +212,34 @@ jQuery.Animation = jQuery.extend( Animation, { ...@@ -212,12 +212,34 @@ jQuery.Animation = jQuery.extend( Animation, {
}); });
function defaultPrefilter( elem, props, opts ) { 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, style = elem.style,
orig = {}, orig = {},
handled = [], handled = [],
hidden = elem.nodeType && isHidden( elem ); 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 // height/width overflow pass
if ( elem.nodeType === 1 && ( props.height || props.width ) ) { if ( elem.nodeType === 1 && ( props.height || props.width ) ) {
// Make sure that nothing sneaks out // Make sure that nothing sneaks out
...@@ -244,7 +266,7 @@ function defaultPrefilter( elem, props, opts ) { ...@@ -244,7 +266,7 @@ function defaultPrefilter( elem, props, opts ) {
if ( opts.overflow ) { if ( opts.overflow ) {
style.overflow = "hidden"; style.overflow = "hidden";
this.finish(function() { anim.finish(function() {
style.overflow = opts.overflow[ 0 ]; style.overflow = opts.overflow[ 0 ];
style.overflowX = opts.overflow[ 1 ]; style.overflowX = opts.overflow[ 1 ];
style.overflowY = opts.overflow[ 2 ]; style.overflowY = opts.overflow[ 2 ];
...@@ -270,11 +292,11 @@ function defaultPrefilter( elem, props, opts ) { ...@@ -270,11 +292,11 @@ function defaultPrefilter( elem, props, opts ) {
if ( hidden ) { if ( hidden ) {
showHide([ elem ], true ); showHide([ elem ], true );
} else { } else {
this.finish(function() { anim.finish(function() {
showHide([ elem ]); showHide([ elem ]);
}); });
} }
this.finish(function() { anim.finish(function() {
var prop; var prop;
jQuery.removeData( elem, "fxshow", true ); jQuery.removeData( elem, "fxshow", true );
for ( prop in orig ) { for ( prop in orig ) {
...@@ -283,7 +305,7 @@ function defaultPrefilter( elem, props, opts ) { ...@@ -283,7 +305,7 @@ function defaultPrefilter( elem, props, opts ) {
}); });
for ( index = 0 ; index < length ; index++ ) { for ( index = 0 ; index < length ; index++ ) {
prop = handled[ 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 ); orig[ prop ] = dataShow[ prop ] || jQuery.style( elem, prop );
if ( !( prop in dataShow ) ) { if ( !( prop in dataShow ) ) {
...@@ -482,10 +504,10 @@ jQuery.fn.extend({ ...@@ -482,10 +504,10 @@ jQuery.fn.extend({
this.queue( optall.queue, doAnimation ); this.queue( optall.queue, doAnimation );
}, },
stop: function( type, clearQueue, gotoEnd ) { stop: function( type, clearQueue, gotoEnd ) {
var stopQueue = function( elem, data, index ) { var stopQueue = function( hooks ) {
var hooks = data[ index ]; var stop = hooks.stop;
jQuery.removeData( elem, index, true ); delete hooks.stop;
hooks.stop( gotoEnd ); stop( gotoEnd );
}; };
if ( typeof type !== "string" ) { if ( typeof type !== "string" ) {
...@@ -498,30 +520,27 @@ jQuery.fn.extend({ ...@@ -498,30 +520,27 @@ jQuery.fn.extend({
} }
return this.each(function() { return this.each(function() {
var index, var dequeue = true,
hadTimers = false, index = type != null && type + "queueHooks",
timers = jQuery.timers, timers = jQuery.timers,
data = jQuery._data( this ); data = jQuery._data( this );
// clear marker counters if we know they won't be if ( index ) {
if ( !gotoEnd ) { if ( data[ index ] && data[ index ].stop ) {
jQuery._unmark( true, this ); stopQueue( data[ index ] );
} }
} else {
if ( type == null ) {
for ( index in data ) { for ( index in data ) {
if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { 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--; ) { for ( index = timers.length; index--; ) {
if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {
timers[ index ].anim.stop( gotoEnd ); timers[ index ].anim.stop( gotoEnd );
hadTimers = true; dequeue = false;
timers.splice( index, 1 ); timers.splice( index, 1 );
} }
} }
...@@ -529,7 +548,7 @@ jQuery.fn.extend({ ...@@ -529,7 +548,7 @@ jQuery.fn.extend({
// start the next in the queue if the last step wasn't forced // start the next in the queue if the last step wasn't forced
// timers currently will call their complete callbacks, which will dequeue // timers currently will call their complete callbacks, which will dequeue
// but only if they were gotoEnd // but only if they were gotoEnd
if ( !( gotoEnd && hadTimers ) ) { if ( dequeue || !gotoEnd ) {
jQuery.dequeue( this, type ); jQuery.dequeue( this, type );
} }
}); });
...@@ -589,15 +608,13 @@ jQuery.speed = function( speed, easing, fn ) { ...@@ -589,15 +608,13 @@ jQuery.speed = function( speed, easing, fn ) {
// Queueing // Queueing
opt.old = opt.complete; opt.old = opt.complete;
opt.complete = function( noUnmark ) { opt.complete = function() {
if ( jQuery.isFunction( opt.old ) ) { if ( jQuery.isFunction( opt.old ) ) {
opt.old.call( this ); opt.old.call( this );
} }
if ( opt.queue ) { if ( opt.queue ) {
jQuery.dequeue( this, opt.queue ); jQuery.dequeue( this, opt.queue );
} else if ( noUnmark !== false ) {
jQuery._unmark( this );
} }
}; };
......
(function( jQuery ) { (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({ 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 ) { queue: function( elem, type, data ) {
var q; var queue;
if ( elem ) { if ( elem ) {
type = ( type || "fx" ) + "queue"; 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 // Speed up dequeue by getting out quickly if this is just a lookup
if ( data ) { if ( data ) {
if ( !q || jQuery.isArray(data) ) { if ( !queue || jQuery.isArray(data) ) {
q = jQuery._data( elem, type, jQuery.makeArray(data) ); queue = jQuery._data( elem, type, jQuery.makeArray(data) );
} else { } else {
q.push( data ); queue.push( data );
} }
} }
return q || []; return queue || [];
} }
}, },
...@@ -71,7 +25,10 @@ jQuery.extend({ ...@@ -71,7 +25,10 @@ jQuery.extend({
var queue = jQuery.queue( elem, type ), var queue = jQuery.queue( elem, type ),
fn = queue.shift(), 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 the fx queue is dequeued, always remove the progress sentinel
if ( fn === "inprogress" ) { if ( fn === "inprogress" ) {
...@@ -79,22 +36,31 @@ jQuery.extend({ ...@@ -79,22 +36,31 @@ jQuery.extend({
} }
if ( fn ) { if ( fn ) {
// Add a progress sentinel to prevent the fx queue from being // Add a progress sentinel to prevent the fx queue from being
// automatically dequeued // automatically dequeued
if ( type === "fx" ) { if ( type === "fx" ) {
queue.unshift( "inprogress" ); queue.unshift( "inprogress" );
} }
jQuery._data( elem, type + ".run", hooks ); // clear up the last queue stop function
fn.call( elem, function() { delete hooks.stop;
jQuery.dequeue( elem, type ); fn.call( elem, next, hooks );
}, hooks );
} }
if ( !queue.length && hooks ) {
if ( !queue.length ) { hooks.empty.fire();
jQuery.removeData( elem, type + "queue " + type + ".run", true );
handleQueueMarkDefer( elem, type, "queue" );
} }
},
// 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({ ...@@ -117,6 +83,9 @@ jQuery.fn.extend({
this.each(function() { this.each(function() {
var queue = jQuery.queue( this, type, data ); var queue = jQuery.queue( this, type, data );
// ensure a hooks for this queue
jQuery._queueHooks( this, type );
if ( type === "fx" && queue[0] !== "inprogress" ) { if ( type === "fx" && queue[0] !== "inprogress" ) {
jQuery.dequeue( this, type ); jQuery.dequeue( this, type );
} }
...@@ -146,31 +115,27 @@ jQuery.fn.extend({ ...@@ -146,31 +115,27 @@ jQuery.fn.extend({
// Get a promise resolved when queues of a certain type // Get a promise resolved when queues of a certain type
// are emptied (fx is the type by default) // are emptied (fx is the type by default)
promise: function( type, object ) { promise: function( type, object ) {
if ( typeof type !== "string" ) { var tmp,
object = type;
type = undefined;
}
type = type || "fx";
var defer = jQuery.Deferred(),
elements = this,
i = elements.length,
count = 1, count = 1,
deferDataKey = type + "defer", defer = jQuery.Deferred(),
queueDataKey = type + "queue", elements = this,
markDataKey = type + "mark", i = this.length,
tmp; resolve = function() {
function resolve() {
if ( !( --count ) ) { if ( !( --count ) ) {
defer.resolveWith( elements, [ elements ] ); defer.resolveWith( elements, [ elements ] );
} }
};
if ( typeof type !== "string" ) {
object = type;
type = undefined;
} }
type = type || "fx";
while( i-- ) { while( i-- ) {
if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || if ( (tmp = jQuery._data( elements[ i ], type + "queueHooks" )) && tmp.empty ) {
( jQuery.data( elements[ i ], queueDataKey, undefined, true ) ||
jQuery.data( elements[ i ], markDataKey, undefined, true ) ) &&
jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) {
count++; count++;
tmp.add( resolve ); tmp.empty.add( resolve );
} }
} }
resolve(); resolve();
......
...@@ -1628,3 +1628,84 @@ asyncTest( "hide, fadeOut and slideUp called on element width height and width = ...@@ -1628,3 +1628,84 @@ asyncTest( "hide, fadeOut and slideUp called on element width height and width =
start(); 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
module("queue", { teardown: moduleTeardown }); module( "queue", {
teardown: moduleTeardown
});
test("queue() with other types",function() { test( "queue() with other types", 12, function() {
expect(12);
var counter = 0; var counter = 0;
stop(); stop();
var $div = jQuery({}), var $div = jQuery({}),
defer; defer;
$div.promise( "foo" ).done(function() {
$div.promise("foo").done(function() {
equal( counter, 0, "Deferred for collection with no queue is automatically resolved" ); equal( counter, 0, "Deferred for collection with no queue is automatically resolved" );
}); });
...@@ -216,85 +216,46 @@ test("clearQueue() clears the fx queue", function() { ...@@ -216,85 +216,46 @@ test("clearQueue() clears the fx queue", function() {
div.removeData(); div.removeData();
}); });
test("_mark() and _unmark()", function() { asyncTest( "fn.promise() - called when fx queue is empty", 3, function() {
expect(1); var foo = jQuery( "#foo" ).clone().andSelf(),
promised = false;
var div = {},
$div = jQuery( div );
stop(); foo.queue( function( next ) {
// called twice!
jQuery._mark( div, "foo" ); ok( !promised, "Promised hasn't been called" );
jQuery._mark( div, "foo" ); setTimeout( next, 10 );
jQuery._unmark( div, "foo" );
jQuery._unmark( div, "foo" );
$div.promise( "foo" ).done(function() {
ok( true, "No more marks" );
start();
}); });
}); foo.promise().done( function() {
ok( promised = true, "Promised" );
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" );
start(); start();
}); });
}); });
test("promise()", function() { asyncTest( "fn.promise( \"queue\" ) - called whenever last queue function is dequeued", 5, function() {
expect(1); var foo = jQuery( "#foo" ),
test;
stop(); foo.promise( "queue" ).done( function() {
strictEqual( test, undefined, "called immediately when queue was already empty" );
var objects = []; });
test = 1;
jQuery.each( [{}, {}], function( i, div ) { foo.queue( "queue", function( next ) {
var $div = jQuery( div ); strictEqual( test++, 1, "step one" );
$div.queue(function( next ) { setTimeout( next, 0 );
setTimeout( function() { }).queue( "queue", function( next ) {
if ( i ) { strictEqual( test++, 2, "step two" );
next();
setTimeout( function() {
jQuery._unmark( div );
}, 20 );
} else {
jQuery._unmark( div );
setTimeout( function() { setTimeout( function() {
strictEqual( test++, 4, "step four" );
next(); next();
}, 20 );
}
}, 50 );
}).queue(function( next ) {
next();
});
jQuery._mark( div );
objects.push( $div );
});
jQuery.when.apply( jQuery, objects ).done(function() {
ok( true, "Deferred resolved" );
start(); start();
}, 10 );
}).promise( "queue" ).done( function() {
strictEqual( test++, 3, "step three" );
}); });
jQuery.each( objects, function() { foo.dequeue( "queue" );
this.dequeue();
});
}); });
test(".promise(obj)", function() { test( ".promise(obj)", function() {
expect(2); expect(2);
var obj = {}; var obj = {};
...@@ -303,3 +264,23 @@ test(".promise(obj)", function() { ...@@ -303,3 +264,23 @@ test(".promise(obj)", function() {
ok( jQuery.isFunction( promise.promise ), ".promise(type, obj) returns a promise" ); ok( jQuery.isFunction( promise.promise ), ".promise(type, obj) returns a promise" );
strictEqual( promise, obj, ".promise(type, obj) returns obj" ); 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 );
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment