jQuery 源碼解析(三十) 動畫模塊 $.animate()詳解

jQuery的動畫模塊提供了包括隱藏顯示動畫、漸顯漸隱動畫、滑入劃出動畫,同時還支持構造複雜自定義動畫,動畫模塊用到了以前講解過的不少其它不少模塊,例如隊列、事件等等,css

$.animate()的用法以下:animate(prop,speed,easing,callback),參數以下:html

  • prop   ;對象,               ;包含將要動畫的樣式
  • speed   ;字符串或數字    ;動畫運行持續時間,單位毫秒,能夠設置爲"slow"、"normal"、"fast"等預約義時間
  • easing  ;字符串            ;表示所使用的緩動函數
  • callback  ;回調函數           ;當動畫完成時被調用

仍是舉個栗子:node

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="http://libs.baidu.com/jquery/1.11.1/jquery.min.js"></script>
    <style>
        div{width: 200px;height: 50px;background: #cfc;}
    </style>
</head>
<body>
    <button>按鈕</button>
    <div></div>
    <script>
        $('button').click(function(){
            $('div').animate({width:'400px',height:'100px',background:'#f00'},2000,'linear',function(){alert('動畫完成成功')})
        })
    </script>    
</body>
</html>

咱們定義一個div和一個按鈕,再在按鈕上綁定一個事件,該事件回調用$.animate()來修改div的樣式,動畫完成後再執行一個回調函數,效果以下:jquery

挺好用的啊,很方便的,能夠用jQuery實現各類動畫,尺寸、位置、顏色,還能夠配合CSS3的transform屬性實現各類酷炫效果。數組

 

源碼分析緩存


jQuery內在執行$.animate()時會遍歷參數1也就是咱們要修改的樣式,而後分別建立一個對應的$.fx對象,實際上最終的動畫是在$.fx這個對象上運行的,另外jQuery內部會維護一個setInterval(),默認會每隔13毫秒執行一次樣式的修改,這樣看起來就和動畫同樣了。閉包

直接看代碼吧,$.animate()實現以下:ide

jQuery.fn.extend({
    animate: function( prop, speed, easing, callback ) {                //動畫入口函數,負責執行單個樣式的動畫,
        var optall = jQuery.speed( speed, easing, callback );                //調用$.speed修正運行時間、緩動函數,重寫完成回調函數

        if ( jQuery.isEmptyObject( prop ) ) {                                //若是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() {                                            //執行動畫的函數,後面再講解,咱們先把流程理解了
            /**/
        }

        return optall.queue === false ?                                        //queue表示是否將當前動畫放入隊列
            this.each( doAnimation ) :                                             //若是optall.queue爲false,則調用.each()方法在每一個匹配元素上執行動畫函數doAnimation();
            this.queue( optall.queue, doAnimation );                             //不然調用方法.queue()將動畫函數doAnimation()插入選項queue指定的隊列中。
    },
})

$.queue以前已經講過了,能夠看這個鏈接http://www.javashuo.com/article/p-kjugeufe-mg.html,因爲插入的是默認動畫,所以插入到隊列後就會自動執行的。函數

$.speed()是用於負責修正運行時間、緩動函數,重寫完成回調函數的,以下:源碼分析

jQuery.extend({
    speed: function( speed, easing, fn ) {         //負責修正運行時間、緩動函數,重寫完成回調函數。//speed:表示動畫持續時間、easing是所使用的緩動函數、fn是動畫完成時被調用的函數。
        var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {     //若是speed是對象,即$.fn.animate()只傳入了兩個參數的格式,則將該對象賦值給opt對象。
            complete: fn || !fn && easing ||                                                     //依次嘗試把最後一個參數做爲完成回調函數保存到complete中
                jQuery.isFunction( speed ) && speed,
            duration: speed,                                                                     //動畫持續時間
            easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing             //若是參數3非空,則設置easing爲參數2  
        };

        opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :     //修正duration選項
            opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;

        // normalize opt.queue - true/undefined/null -> "fx"
        if ( opt.queue == null || opt.queue === true ) {
            opt.queue = "fx";
        }

        // Queueing 
        opt.old = opt.complete;                                                                 //備份完成回調函數complete到old屬性中

        opt.complete = function( noUnmark ) {                                                     //重寫complete函數,當前動畫完成時,自動取出下一個動畫函數。
            if ( jQuery.isFunction( opt.old ) ) {
                opt.old.call( this );
            }

            if ( opt.queue ) {
                jQuery.dequeue( this, opt.queue );
            } else if ( noUnmark !== false ) {
                jQuery._unmark( this );
            }
        };

        return opt;                                                                             //最後返回opt
    },
})

$.animate函數很是靈活,能夠有以下形式:

  • animate(prop,speed,easing,callback)   
  • animate(prop,easing,callback)      
  • animate(prop,speed,callback)       
  • animate(prop,callback)           
  • animate(prop,obj)           

 這就是$.speed函數的功勞了。

回到$.animate函數內,當將doAnimation插入到隊列後,因爲是默認隊列就會自動執行該函數,doAnimation實現以下:

function doAnimation() {                    //負責遍歷動畫樣式集合,先修正、解析、備份樣式名,而後爲每一個樣式調用構造函數jQuery.fx()構造動畫
    // XXX 'this' does not always have a nodeName when running the
    // test suite

    if ( optall.queue === false ) {                //若是選項queue爲false,動畫將會當即執行
        jQuery._mark( this );
    }

    var opt = jQuery.extend( {}, optall ),                    //opt是完整選項集optall的副本;
        isElement = this.nodeType === 1,                    //isElement用於檢測當前元素是不是DOM元素
        hidden = isElement && jQuery(this).is(":hidden"),    //hidden表示當前元素是不是可見的
        name, val, p, e,
        parts, start, end, unit,
        method;

    // will store per property easing and be used to determine when an animation is complete
    opt.animatedProperties = {};                            //存放動畫樣式的緩存函數,當動畫完成後,屬性值被設置爲true。

    for ( p in prop ) {                                        //遍歷動畫樣式集合prop。修正、解析、備份樣式        ;p是每個樣式的名稱,好比:width height

        // property name normalization
        name = jQuery.camelCase( p );                            ///將樣式名格式化爲駝峯式
        if ( p !== name ) {
            prop[ name ] = prop[ p ];
            delete prop[ p ];
        }

        val = prop[ name ];

        //獲取每一個樣式的結束值和緩動函數
        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 ) {    //若是值是hide但當前元素不可見,或者值是show但當前元素可見,表示不須要執行動畫就已經完成了
            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 ) {         //若是opt.overflow不爲空,即樣式值是width或height、
        this.style.overflow = "hidden";     //則設置樣式overflow爲'hidden',即超出部分隱藏,這樣不影響其餘佈局
    }

    for ( p in prop ) {                 //再次遍歷動畫樣式集合prop,爲每一個屬性執行動畫效果 p是每一個樣式名,好比:width、height
        e = new jQuery.fx( this, opt, p );     //調用構造函數jQuery.fn(elem,options,prop)建立一個標準動畫對象
        val = prop[ p ];                     //獲取設置的結束值,好比:500px,600px

        if ( rfxtypes.test( val ) ) {         //若是樣式值val是toggle、show或hide

            // 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 {                             //若是樣式值是數值型,則計算開始值start、結束值end、單位unit,而後開始執行動畫
            parts = rfxnum.exec( val );         //rfxnum用於匹配樣式值中的運算符、數值、單位。rfxnum = /^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,    格式:Array [ "700px", 符號, "數值", "單位" ] ,好比:'+=20px',匹配後:Array [ "+=20px", "+=", "20", "px" ]
            start = e.cur();                     //獲取初始值,好比:200

            if ( parts ) {                         //若是樣式值可以匹配正則parts,則說明是數值型,就計算開始值start、結束值end、單位unit,而後開始執行動畫。
                end = parseFloat( parts[2] );                                         //end是傳入的樣式的數值,多是結束值,也可能要加上或減去的值
                unit = parts[3] || ( jQuery.cssNumber[ p ] ? "" : "px" );             //unit是最後的單位,若是沒有傳入則設置爲px單位

                // We need to compute starting value
                if ( unit !== "px" ) {                                                 //若是結束值的單位不是默認單位px,則將開始值start換算爲與結束值end的單位一致的數值。
                    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 );                                         //調用jQuery.fx.prototype.custom(from,to,unit)開始執行動畫。start是開始值,end是結束值,unit是單位。

            } else {
                e.custom( start, val, "" );                                     //樣式值不是數值型時,仍然開始執行動畫,以支持插件擴展的動畫樣式。
            }
        }
    }

    // For JS strict compliance
    return true;
}

函數內首先調用new jQuery.fx()建立一個$.fx對象,最後調用該對象的custom開始執行動畫,$.fx的定義以下:

jQuery.extend({
    fx: function( elem, options, prop ) {         //jQuery構造函數,用於爲單個樣式構造動畫對象。elem:當前匹配元素、options:當前動畫的選項集、prop:參與動畫的當前樣式。
        this.options = options;
        this.elem = elem;
        this.prop = prop;

        options.orig = options.orig || {};
    }
})

就是在當前實例上掛載幾個屬性,它的其它方法都定義在原型上的,例如cur和custom以下:

jQuery.fx.prototype = {
    // Get the current size
    cur: function() {             //獲取this.elem元素this.prop樣式當前的值
        if ( this.elem[ this.prop ] != null && (!this.elem.style || this.elem.style[ this.prop ] == null) ) {     //若是當前DOM元素的this.prop屬性不爲空,且沒有style屬性或者有style屬性可是style[this.prop]爲null
            return this.elem[ this.prop ];                                                                                 //
        }

        var parsed,
            r = jQuery.css( this.elem, this.prop );                             //獲取該DOM元素的prop樣式的值,好比:200px
        // Empty strings, null, undefined and "auto" are converted to 0,
        // complex values such as "rotate(1rad)" are returned as is,
        // simple values such as "10px" are parsed to Float.
        return isNaN( parsed = parseFloat( r ) ) ? !r || r === "auto" ? 0 : r : parsed;
    },

    // Start an animation from one number to another
    custom: function( from, to, unit ) {                                     //負責開始執行單個樣式的動畫。from:當前樣式的開始值、to:當前樣式的結束值、unit:當前樣式的單位。
        var self = this,                                                         //self是當前樣式的jQuery.fx對象
            fx = jQuery.fx;

        this.startTime = fxNow || createFxNow();                                 //動畫開始的時間,值爲當前時間,每次調用animate()方法時多個樣式的該值是同樣的。
        this.end = to;                                                            //當前樣式的結束值
        this.now = this.start = from;                                            //當前樣式的單位。
        this.pos = this.state = 0;                                                //this.now:當前幀樣式值    、this.start:當前樣式的開始值
        this.unit = unit || this.unit || ( jQuery.cssNumber[ this.prop ] ? "" : "px" );     //this.pos:差值百分比。this.state:已執行時間的百分比    

        function t( gotoEnd ) {                                                 //構造封裝了jQuery.fx.prototype.step()的單幀閉包動畫函數,函數t經過閉包機制引用jQuery.fx()對象。
            return self.step( gotoEnd );
        }

        t.queue = this.options.queue;         //動畫類型,默認爲:fx
        t.elem = this.elem;                 //elem是當前匹配元素
        t.saveState = function() {             //記錄當前樣式動畫的狀態,在stop()中用的到
            if ( self.options.hide && jQuery._data( self.elem, "fxshow" + self.prop ) === undefined ) {     
                jQuery._data( self.elem, "fxshow" + self.prop, self.start );
            }
        };

        if ( t() && jQuery.timers.push(t) && !timerId ) {                         //執行單幀閉包動畫函數t(gotoEnd),若是該函數返回true則表示動畫還沒有完成,則並將其插入全局動畫函數數組jQuery.timers中,若是timeId不存在,timeId是執行動畫的全局定時器。
            timerId = setInterval( fx.tick, fx.interval );                             //建立一個定時器。週期性的執行jQuery.fx.tick()方法,從而開始執行動畫。
        }
    },

$.tick()內會執行setInterval,隔13毫秒就執行一次 t() 函數,t函數內會執行step()函數,該函數會計算匹配元素下一次的樣式值,以下:

 writer by:大沙漠 QQ:22969969

jQuery.fx.prototype = {
    step: function( gotoEnd ) {             //負責計算和執行當前樣式動畫的一幀。
        var p, n, complete,
            t = fxNow || createFxNow(),         //t是當前時間
            done = true,
            elem = this.elem,                     //匹配元素
            options = this.options;             //選項

        if ( gotoEnd || t >= options.duration + this.startTime ) {     //動畫已完成的邏輯 若是參數totoEnd爲true(stop())調用時),或超出了動畫完成時間,最後會返回false
            this.now = this.end;                                         //設置當前幀式值爲結束值
            this.pos = this.state = 1;                                     //設置已執行時間百分比和差值百分比爲1
            this.update();                                                 //調用update()更新樣式值。這三行代碼是用來修正動畫可能致使的樣式偏差的。

            options.animatedProperties[ this.prop ] = true;             //設置options.animatedProperties[ this.prop ]爲true,表示當前樣式動畫已經執行完成。

            for ( p in options.animatedProperties ) {
                if ( options.animatedProperties[ p ] !== true ) {
                    done = false;
                }
            }

            if ( done ) {
                // Reset the overflow
                if ( options.overflow != null && !jQuery.support.shrinkWrapBlocks ) {

                    jQuery.each( [ "", "X", "Y" ], function( index, value ) {
                        elem.style[ "overflow" + value ] = options.overflow[ index ];
                    });
                }

                // Hide the element if the "hide" operation was done
                if ( options.hide ) {
                    jQuery( elem ).hide();
                }

                // Reset the properties, if the item has been hidden or shown
                if ( options.hide || options.show ) {
                    for ( p in options.animatedProperties ) {
                        jQuery.style( elem, p, options.orig[ p ] );
                        jQuery.removeData( elem, "fxshow" + p, true );
                        // Toggle data is no longer needed
                        jQuery.removeData( elem, "toggle" + p, true );
                    }
                }

                // Execute the complete function
                // in the event that the complete function throws an exception
                // we must ensure it won't be called twice. #5684

                complete = options.complete;
                if ( complete ) {

                    options.complete = false;
                    complete.call( elem );
                }
            }

            return false;

        } else {                                                     //若是當前樣式動畫還沒有完成
            // classical easing cannot be used with an Infinity duration
            if ( options.duration == Infinity ) {
                this.now = t;
            } else {
                n = t - this.startTime;                             //n = 當前時間-開始時間 = 已執行時間
                this.state = n / options.duration;                     //已執行時間百分比 = 已執行時間/運行時間

                // Perform the easing function, defaults to swing
                this.pos = jQuery.easing[ options.animatedProperties[this.prop] ]( this.state, n, 0, 1, options.duration );     //差值百分比=緩動函數(已執行時間百分比,已執行時間,0,1,運行時間)
                this.now = this.start + ( (this.end - this.start) * this.pos );                                                 //當前幀樣式值=開始值+差值百分比*(結束值-開始值)
            }
            // Perform the next step of the animation
            this.update();                                                                                                         //更新樣式值。
        }

        return true;
    }
}

計算出樣式值後會將值保存到this.now上面,最終調用this.update()進行更新樣式,update源碼以下:

jQuery.fx.prototype = {
    update: function() {                 //更新當前樣式的值。內部經過jQuery.fx.step中的方法來更新樣式的值
        if ( this.options.step ) {                 //若是當前的animate指定了step回調函數
            this.options.step.call( this.elem, this.now, this );     //則調用該回調函數,參數分別是當前樣式的值和元素的引用
        }

        ( jQuery.fx.step[ this.prop ] || jQuery.fx.step._default )( this );     //不然調用默認方法jQuery.fx.step._default()直接設置內聯樣式(style屬性)或DOM屬性。
    },
}

能夠看到優先經過$.fx.step來進行修改樣式,其次經過$.fx.step._default來修改,以下:

jQuery.extend( jQuery.fx, {
    step: {                     //最後的步驟:更新樣式
        opacity: function( fx ) {         //修正樣式opacity的設置行爲
            jQuery.style( fx.elem, "opacity", fx.now );
        },

        _default: function( fx ) {         //默認狀況下,經過直接設置內聯屬性(style屬性)或DOM屬性更新樣式值。
            if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) {     //若是當前元素有style屬性,而且syle含有指定的樣式
                fx.elem.style[ fx.prop ] = fx.now + fx.unit;                 //在style屬性上設置樣式值
            } else {
                fx.elem[ fx.prop ] = fx.now;                               //不然在DOM元素上設置DOM屬性,例如:scrollTop、scrollLeft
            }
        }
    }
})

能夠看到最終就是修改的元素的style來修改樣式的,或者直接修改DOM對象屬性。

相關文章
相關標籤/搜索