qwertymk
qwertymk

Reputation: 35294

Where does jQuery do animations/timers in `$.queue()`?

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

Answers (1)

zzzzBov
zzzzBov

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.


Reference

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

Related Questions