Reputation: 35294
I was looking through the source of jQuery (specifically the queue()
function) and saw that in just puts the function into a the .data()
object associated with that element:
queue: function( elem, type, data ) {
var q;
if ( elem ) {
type = ( type || "fx" ) + "queue";
q = 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) );
} else {
q.push( data );
}
}
return q || [];
}
}
Now looking at the ._data
which is just .data()
with a fourth argument of true, where are any timers or animations being set? Or any function calls for that matter:
data: function( elem, name, data, pvt /* Internal Use Only */ ) {
if ( !jQuery.acceptData( elem ) ) {
return;
}
var privateCache, thisCache, ret,
internalKey = jQuery.expando,
getByName = typeof name === "string",
// We have to handle DOM nodes and JS objects differently because IE6-7
// can't GC object references properly across the DOM-JS boundary
isNode = elem.nodeType,
// Only DOM nodes need the global jQuery cache; JS object data is
// attached directly to the object so GC can occur automatically
cache = isNode ? jQuery.cache : elem,
// Only defining an ID for JS objects if its cache already exists allows
// the code to shortcut on the same path as a DOM node with no cache
id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey,
isEvents = name === "events";
// Avoid doing any more work than we need to when trying to get data on an
// object that has no data at all
if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) {
return;
}
if ( !id ) {
// Only DOM nodes need a new unique ID for each element since their data
// ends up in the global cache
if ( isNode ) {
elem[ internalKey ] = id = ++jQuery.uuid;
} else {
id = internalKey;
}
}
if ( !cache[ id ] ) {
cache[ id ] = {};
// Avoids exposing jQuery metadata on plain JS objects when the object
// is serialized using JSON.stringify
if ( !isNode ) {
cache[ id ].toJSON = jQuery.noop;
}
}
// An object can be passed to jQuery.data instead of a key/value pair; this gets
// shallow copied over onto the existing cache
if ( typeof name === "object" || typeof name === "function" ) {
if ( pvt ) {
cache[ id ] = jQuery.extend( cache[ id ], name );
} else {
cache[ id ].data = jQuery.extend( cache[ id ].data, name );
}
}
privateCache = thisCache = cache[ id ];
// jQuery data() is stored in a separate object inside the object's internal data
// cache in order to avoid key collisions between internal data and user-defined
// data.
if ( !pvt ) {
if ( !thisCache.data ) {
thisCache.data = {};
}
thisCache = thisCache.data;
}
if ( data !== undefined ) {
thisCache[ jQuery.camelCase( name ) ] = data;
}
// Users should not attempt to inspect the internal events object using jQuery.data,
// it is undocumented and subject to change. But does anyone listen? No.
if ( isEvents && !thisCache[ name ] ) {
return privateCache.events;
}
// Check for both converted-to-camel and non-converted data property names
// If a data property was specified
if ( getByName ) {
// First Try to find as-is property data
ret = thisCache[ name ];
// Test for null|undefined property data
if ( ret == null ) {
// Try to find the camelCased property
ret = thisCache[ jQuery.camelCase( name ) ];
}
} else {
ret = thisCache;
}
return ret;
}
EDIT for zzzzBov:
animate: function( prop, speed, easing, callback ) {
var optall = jQuery.speed( speed, easing, callback );
if ( jQuery.isEmptyObject( prop ) ) {
return this.each( optall.complete, [ false ] );
....
return optall.queue === false ?
this.each( doAnimation ) :
this.queue( optall.queue, doAnimation );
Upvotes: 1
Views: 837
Reputation: 179186
jQuery's animate
method is a complex beast (see reference below). It starts by normalizing the parameters, and then immediately jumps into the doAnimation
function which is the callback used by jQuery's queue
method. jQuery queue's animations so they happen in order. The queue doesn't animate anything by itself, it simply acts as a trigger for the code that performs the animation.
At the end of doAnimation
, there is a loop to animate every relevant property in the animation (lines 8576 - 8618, v1.7.2). The first inner-line of the loop instantiates a new jQuery.fx
object:
e = new jQuery.fx( this, opt, p );
At the end of the loop, e.custom(...)
is called in a couple places. This is the important function. If you look through jQuery.fx.prototype.custom
(see reference below), you'll find:
if ( t() && jQuery.timers.push(t) && !timerId ) {
timerId = setInterval( fx.tick, fx.interval );
}
The line with setInterval
is where jQuery's animation heartbeat is started. This points to jQuery.fx.tick
(see reference below) which iterates through every timer in jQuery.timers
. If you look at the snippet above, you'll notice that part of the if
statement involves pushing t
into the stack of timers. t
was set in jQuery.fx.prototype.custom
as:
function t( gotoEnd ) {
return self.step( gotoEnd );
}
And that right there is where jQuery's animation happens.
jQuery's animate
function (lines 8484 - 8627, v1.7.2):
animate: function( prop, speed, easing, callback ) {
var optall = jQuery.speed( speed, easing, callback );
if ( jQuery.isEmptyObject( prop ) ) {
return this.each( optall.complete, [ false ] );
}
// Do not change referenced properties as per-property easing will be lost
prop = jQuery.extend( {}, prop );
function doAnimation() {
// XXX 'this' does not always have a nodeName when running the
// test suite
if ( optall.queue === false ) {
jQuery._mark( this );
}
var opt = jQuery.extend( {}, optall ),
isElement = this.nodeType === 1,
hidden = isElement && jQuery(this).is(":hidden"),
name, val, p, e, hooks, replace,
parts, start, end, unit,
method;
// will store per property easing and be used to determine when an animation is complete
opt.animatedProperties = {};
// first pass over propertys to expand / normalize
for ( p in prop ) {
name = jQuery.camelCase( p );
if ( p !== name ) {
prop[ name ] = prop[ p ];
delete prop[ p ];
}
if ( ( hooks = jQuery.cssHooks[ name ] ) && "expand" in hooks ) {
replace = hooks.expand( prop[ name ] );
delete prop[ name ];
// not quite $.extend, this wont overwrite keys already present.
// also - reusing 'p' from above because we have the correct "name"
for ( p in replace ) {
if ( ! ( p in prop ) ) {
prop[ p ] = replace[ p ];
}
}
}
}
for ( name in prop ) {
val = prop[ name ];
// easing resolution: per property > opt.specialEasing > opt.easing > 'swing' (default)
if ( jQuery.isArray( val ) ) {
opt.animatedProperties[ name ] = val[ 1 ];
val = prop[ name ] = val[ 0 ];
} else {
opt.animatedProperties[ name ] = opt.specialEasing && opt.specialEasing[ name ] || opt.easing || 'swing';
}
if ( val === "hide" && hidden || val === "show" && !hidden ) {
return opt.complete.call( this );
}
if ( isElement && ( name === "height" || name === "width" ) ) {
// Make sure that nothing sneaks out
// Record all 3 overflow attributes because IE does not
// change the overflow attribute when overflowX and
// overflowY are set to the same value
opt.overflow = [ this.style.overflow, this.style.overflowX, this.style.overflowY ];
// Set display property to inline-block for height/width
// animations on inline elements that are having width/height animated
if ( jQuery.css( this, "display" ) === "inline" &&
jQuery.css( this, "float" ) === "none" ) {
// inline-level elements accept inline-block;
// block-level elements need to be inline with layout
if ( !jQuery.support.inlineBlockNeedsLayout || defaultDisplay( this.nodeName ) === "inline" ) {
this.style.display = "inline-block";
} else {
this.style.zoom = 1;
}
}
}
}
if ( opt.overflow != null ) {
this.style.overflow = "hidden";
}
for ( p in prop ) {
e = new jQuery.fx( this, opt, p );
val = prop[ p ];
if ( rfxtypes.test( val ) ) {
// Tracks whether to show or hide based on private
// data attached to the element
method = jQuery._data( this, "toggle" + p ) || ( val === "toggle" ? hidden ? "show" : "hide" : 0 );
if ( method ) {
jQuery._data( this, "toggle" + p, method === "show" ? "hide" : "show" );
e[ method ]();
} else {
e[ val ]();
}
} else {
parts = rfxnum.exec( val );
start = e.cur();
if ( parts ) {
end = parseFloat( parts[2] );
unit = parts[3] || ( jQuery.cssNumber[ p ] ? "" : "px" );
// We need to compute starting value
if ( unit !== "px" ) {
jQuery.style( this, p, (end || 1) + unit);
start = ( (end || 1) / e.cur() ) * start;
jQuery.style( this, p, start + unit);
}
// If a +=/-= token was provided, we're doing a relative animation
if ( parts[1] ) {
end = ( (parts[ 1 ] === "-=" ? -1 : 1) * end ) + start;
}
e.custom( start, end, unit );
} else {
e.custom( start, val, "" );
}
}
}
// For JS strict compliance
return true;
}
return optall.queue === false ?
this.each( doAnimation ) :
this.queue( optall.queue, doAnimation );
}
Instantiation of jQuery.fx
(line 8577, v1.7.2):
e = new jQuery.fx( this, opt, p );
jQuery.fx.prototype.custom
(lines 8806 - 8836, v1.7.2):
// Start an animation from one number to another
custom: function( from, to, unit ) {
var self = this,
fx = jQuery.fx;
this.startTime = fxNow || createFxNow();
this.end = to;
this.now = this.start = from;
this.pos = this.state = 0;
this.unit = unit || this.unit || ( jQuery.cssNumber[ this.prop ] ? "" : "px" );
function t( gotoEnd ) {
return self.step( gotoEnd );
}
t.queue = this.options.queue;
t.elem = this.elem;
t.saveState = function() {
if ( jQuery._data( self.elem, "fxshow" + self.prop ) === undefined ) {
if ( self.options.hide ) {
jQuery._data( self.elem, "fxshow" + self.prop, self.start );
} else if ( self.options.show ) {
jQuery._data( self.elem, "fxshow" + self.prop, self.end );
}
}
};
if ( t() && jQuery.timers.push(t) && !timerId ) {
timerId = setInterval( fx.tick, fx.interval );
}
}
jQuery.fx.tick
(lines 8949 - 8965, v1.7.2):
tick: function() {
var timer,
timers = jQuery.timers,
i = 0;
for ( ; i < timers.length; i++ ) {
timer = timers[ i ];
// Checks the timer has not already been removed
if ( !timer() && timers[ i ] === timer ) {
timers.splice( i--, 1 );
}
}
if ( !timers.length ) {
jQuery.fx.stop();
}
}
Upvotes: 3