2017年3月17日 星期五

jq_5


jQuery源碼解析(5)—— Animation動畫

標籤: jqueryanimation動畫源碼
 2204人閱讀 評論(0) 收藏 舉報
 分類:
目錄(?)[+]

閒話

jQuery的動畫機制有800行, 雖然不如樣式的1300行,難度上卻是不減。由於事前不瞭解animate接口的細節使用規則,看代碼期間吃了很多苦頭,尤其是深惡痛絕的defaultPrefilter函數,靠著猜想和數次的逐行攻略,終於全部拿下。本文將一點一點拆解出jq的動畫機制及具體實現,解析各種做法的目的、解耦的方式、必要的結構、增強的輔助功能。
需要提前掌握queue隊列的知識,css樣式機制、data緩存、deferred對象至少要瞭解核心API的功能。有興趣可以參考我之前的幾篇分析。
(本文採用 1.12.0 版本進行講解,用 #number 來標註行號) 

動畫機制

jQuery的動畫機制比較複雜,下面將逐一分析其中要點。

全局Interval

教學時常用的動畫函數demo,結構是下面這樣:
/* demo */

// json -> { prop1: end1, prop2: end2 ...} 屬性與終點的名值對,可一次運動多屬性
function Animation( elem, json, duration, callback ) {

    // some code...

    // 每步運動
    var tick = function() {
        // 對每個屬性算出每步值,並設置。到終點時取消定時器,並執行回調 callback()
    };
    elem.timer = setInterval(tick, 20);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
如何計算每步運動的值需要講究,舉個栗子:
// 方式 1
// 計算次數,算出每次增量(與定時器的設置時間,嚴格相關)
times = duration / 20;
everyTimeAddNum = ( end - start ) / timers;

// 方式 2
// 計算當前流逝的比例,根據比例設置最終值(有定時器即可,與定時時間無關)
passTime = ( +new Date() - createTime ) / duration;
passTime = passTime > 1 ? 1 : passTime;
toValue = ( end - start ) * passTime + start;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
方式2為標準的使用法則,方式1雖然很多人仍在使用包括教學,但是會出現如下兩個問題:
問題1:js單線程
JavaScript是單線程的語言,setTimeout、setInterval定時的向語言的任務隊列添加執行代碼,但是必須等到隊列中已有的代碼執行完畢,若遇到長任務,則拖延明顯。對於」方式1」,若在tick內遞歸的setTimout,tick執行完才會再次setTimeout,每次的延遲都將疊加無法被追償。setInterval也不能倖免,因為js引擎在使用setInterval()時,僅當隊列裡沒有當前定時器的任何其它代碼實例時,才會被添加,而次數和值的累加都是需要函數執行才會生效,因此延遲也無法被追償。
問題2:計時器精度
瀏覽器並不一定嚴格按照設置的時間(比如20ms)來添加下一幀,IE8及以下瀏覽器的精度為15.625ms,IE9+、Chrome精度為4ms,ff和safari約為10ms。對於「方式1」這種把時間拆為確定次數的計算方式,運動速度就一點不精確了。
jQuery顯然採用了」方式2」,而且優化了interval的調用。demo中的方式出現多個動畫時會造成 interval 滿天飛的情況,影響性能,既然方式2中動畫邏輯與定時器的時間、調用次數無關,那麼可以單獨抽離,整個動畫機制只使用一個統一的setInterval,把tick推入堆棧jQuery.timers,每次定時器調用jQuery.fx.tick()遍歷堆棧裡的函數,通過tick的返回值知道是否運動完畢,完畢的棧出,沒有動畫的時候就jQuery.fx.stop()暫停。jQuery.fx.start()開啟定時器前會檢測是開啟狀態,防止重複開啟。每次把tick推入堆棧的時候都會調用jQuery.fx.start()。這樣就做到了需要時自動開啟,不需要時自動關閉。
[源碼]
// #672
// jQuery.timers 當前正在運動的動畫的tick函數堆棧
// jQuery.fx.timer() 把tick函數推入堆棧。若已經是最終狀態,則不加入
// jQuery.fx.interval 唯一定時器的定時間隔
// jQuery.fx.start() 開啟唯一的定時器timerId
// jQuery.fx.tick() 被定時器調用,遍歷timers堆棧
// jQuery.fx.stop() 停止定時器,重置timerId=null
// jQuery.fx.speeds 指定了動畫時長duration默認值,和幾個字符串對應的值

// jQuery.fx.off 是用在確定duration時的鉤子,設為true則全局所有動畫duration都會強製為0,直接到結束狀態

// 所有動畫的"每步運動tick函數"都推入timers
jQuery.timers = [];

// 遍歷timers堆棧
jQuery.fx.tick = function() {
    var timer,
        timers = jQuery.timers,
        i = 0;

    // 當前時間毫秒
    fxNow = jQuery.now();

    for ( ; i < timers.length; i++ ) {
        timer = timers[ i ];

        // 每個動畫的tick函數(即此處timer)執行時返回remaining剩餘時間,結束返回false
        // timers[ i ] === timer 的驗證是因為可能有瓜娃子在tick函數中瞎整,刪除jQuery.timers內項目
        if ( !timer() && timers[ i ] === timer ) {
            timers.splice( i--, 1 );
        }
    }

    // 無動畫了,則stop掉全局定時器timerId
    if ( !timers.length ) {
        jQuery.fx.stop();
    }
    fxNow = undefined;
};

// 把動畫的tick函數加入$.timers堆棧
jQuery.fx.timer = function( timer ) {
    jQuery.timers.push( timer );
    if ( timer() ) {
        jQuery.fx.start();
    // 若已經在終點了,無需加入
    } else {
        jQuery.timers.pop();
    }
};

// 全局定時器定時間隔
jQuery.fx.interval = 13;

// 啟動全局定時器,定時調用tick遍歷$.timers
jQuery.fx.start = function() {
    // 若已存在,do nothing
    if ( !timerId ) {
        timerId = window.setInterval( jQuery.fx.tick, jQuery.fx.interval );
    }
};

// 停止全局定時器timerId
jQuery.fx.stop = function() {
    window.clearInterval( timerId );
    timerId = null;
};

// speeds(即duration)默認值,和字符串的對應值
jQuery.fx.speeds = {
    slow: 600,
    fast: 200,

    // Default speed,默認
    _default: 400
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76

同步、異步

jQuery.fn.animate

jQuery動畫機制最重要的一個考慮是:動畫間便捷的同步、異步操作。
jQuery允許我們通過$().animate()的形式調用,對應的外觀方法是jQuery.fn.animate( prop, speed, easing, callback ),內部調用動畫的核心函數Animation( elem, properties, options )
上面的demo雖然粗糙,但是思路一致。Animation一經調用,內部的tick函數將被jQuery.fx.timer函數推入jQuery.timers堆棧,立刻開始按照jQuery.fx.interval的間隔運動。要想使動畫異步,就不能立即調用Animation。在回調callback中層層嵌套來完成異步,顯然是極不友好的。jQuery.fn.animate中使用了queue隊列,把Animation函數的調用封裝在doAnimation函數中,通過把doAnimation推入指定的隊列,按照隊列順序異步觸發doAnimation,從而異步調用Animation。
queue隊列是一個堆棧,比如elem的」fx」隊列,jQuery.queue(elem, 「fx」)即為緩存jQuery._data(elem, 「fxqueue」)。每個元素的」fx」隊列都是不同的,因此不同元素或不同隊列之間的動畫是同步的,相同元素且相同隊列之間的動畫是異步的。添加到」fx」隊列的函數若是隊列中當前的第一個函數,將被直接觸發,而添加到其他隊列中的函數需要手動調用jQuery.dequeue才會啟動執行。
如何設置添加的隊列呢?jQuery.fn.animate支持對象參數寫法jQuery.fn.animate( prop, optall),通過 optall.queue指定隊列,未指定隊列的按照默認值」fx」處理。speed、easing、callback均不是必須項,內部通過jQuery.speed將參數統一為對象optall。optall會被綁定上被封裝過的optall.complete函數,調用後執行dequeue調用隊列中下一個doAnimation(後面會講Animation執行完後如何調用complete自動執行下一個動畫)
雖然加入了queue機制後,默認的動畫順序變為了異步而非同步。但optall.queue指定為false時,不使用queue隊列機制,doAnimation將立即調用Animation執行動畫,保留了原有的同步機制。
/* #7888 jQuery.speed
 * 設置參數統一為options對象
---------------------------------------------------------------------- */
// 支持的參數類型(均為可選參數,只有fn會參數提前。無speed設為默認值,無easing在Tween.prototype.init中設為默認值)
// (options)
// (speed [, easing | fn])
// (speed, easing, fn)
// (speed)、(fn)
jQuery.speed = function( speed, easing, fn ) {
    var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
        complete: fn || !fn && easing ||
            jQuery.isFunction( speed ) && speed,
        duration: speed,
        easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
    };

    // jQuery.fx.off控制全局的doAnimation函數生成動畫的時長開關
    opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
        // 支持 "slow" "fast",無值則取默認400
        opt.duration in jQuery.fx.speeds ?
            jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;

    // true/undefined/null -> 設為默認隊列"fx"
    // false不使用隊列機制
    if ( opt.queue == null || opt.queue === true ) {
        opt.queue = "fx";
    }

    opt.old = opt.complete;

    // 對opt.complete進行再封裝
    // 目的是該函數可以dequeue隊列,讓隊列中下個doAnimation開始執行
    opt.complete = function() {
        // 非函數或無值則不調用
        if ( jQuery.isFunction( opt.old ) ) {
            opt.old.call( this );
        }

        // false不使用隊列機制
        if ( opt.queue ) {
            jQuery.dequeue( this, opt.queue );
        }
    };

    return opt;
};


/* #7930 jQuery.fn.animate
 * 外觀方法,對每個elem添加動畫到隊列(默認"fx"隊列,為false不加入隊列直接執行)
---------------------------------------------------------------------- */
jQuery.fn.animate = function( prop, speed, easing, callback ) {
    // 是否有需要動畫的屬性
    var empty = jQuery.isEmptyObject( prop ),
        // 參數修正到對象optall
        optall = jQuery.speed( speed, easing, callback ),
        doAnimation = function() {

            // 執行動畫,返回一個animation對象(後面詳細講)
            var anim = Animation( this, jQuery.extend( {}, prop ), optall );

            // jQuery.fn.finish執行期間jQuery._data( this, "finish" )設置為"finish",所有動畫創建後都必須立即結束到end,即直接運動到最終狀態(後面詳細講)
            if ( empty || jQuery._data( this, "finish" ) ) {
                anim.stop( true );
            }
        };
        // 用於jQuery.fn.finish方法內判斷 queue[ index ] && queue[ index ].finish。比如jQuery.fn.delay(type)添加到隊列的方法沒有finish屬性,不調用直接捨棄
        doAnimation.finish = doAnimation;

    return empty || optall.queue === false ?
        // 直接遍歷執行doAnimation
        this.each( doAnimation ) :
        // 遍歷元素把doAnimation加入對應元素的optall.queue隊列
        this.queue( optall.queue, doAnimation );
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75

jQuery.fn.stop/finish

現在我們有了同步、異步兩種方式,但在同步的時候,有可能出現重複觸發某元素動畫,而我們並不需要。在jq中按照場景可分為:相同隊列正在運動的動畫、所有隊列正在運動的動畫、相同隊列所有的動畫、所有隊列的動畫、非隊列正在運動的動畫。停止動畫分為兩種狀態:直接到運動結束位置、以當前位置結束。
實現原理
清空動畫隊列,調用$(elems).queue( type, [] ),會替換隊列為[],也可以事先保存隊列,然後逐個執行,這正是jQuery.fn.finish的原理。停止當前動畫,jQuery.timers[ index ].anim.stop( gotoEnd )。gotoEnd為布爾值,指定停止動畫到結束位置還是當前位置,通過timers[ index ].elem === this && timers[ index ].queue === type匹配隊列和元素,從這裡也能看出Animation函數中的單步運動tick函數需要綁定elem、anim、queue屬性(anim是Animation返回的animation對象,stop函數用來結束當前動畫,後面會詳細講)。
然而並不是添加到隊列的都是doAnimation,比如jQuery.fn.delay(),由於沒調用Animation,所以沒有tick函數,自然沒有anim.stop,從jq源碼中可以看出,推薦在隊列的hook上綁定hooks.stop停止函數(因此stop/finish中會調用hooks.stop)。queue隊列中被執行的函數備註了的next函數(dequeue操作,調用下一個)和對應的hook對象($._data(type+』queuehook')緩存,empty.fire用於自毀)和this(元素elem),因此可以通過next調用下一項。
/* #8123 jQuery.fn.delay 
 * 動畫延遲函數
---------------------------------------------------------------------- */
jQuery.fn.delay = function( time, type ) {
    time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
    type = type || "fx";

    return this.queue( type, function( next, hooks ) {
        var timeout = window.setTimeout( next, time );
        hooks.stop = function() {
            // 取消延遲調用next
            window.clearTimeout( timeout );
        };
    } );
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
jQuery.fn.stop( type, clearQueue, gotoEnd ):type指定隊列(false會變成」fx」,本方法不能停止非隊列,需要使用jQuery.fn.finish(false));clearQueue為true為清除隊列後續動畫,false為不清除;gotoEnd為true表示直接到運動結束位置,false為當前位置結束
註:type無字符串值時,clearQueue, gotoEnd參數提前,type設為undefined。(對於type為null/undefined的處理很特別。對」fx」按照clearQueue值處理,但是對元素所有隊列動畫都停止,按照goToEnd值處理。非隊列動畫不受影響)
jQuery.fn.finish( type ):type指定隊列(默認」fx」,false表示非隊列),執行過程中標記jQuery._data( this ).finish=true,清空queue隊列,並且遍歷執行隊列中所有doAnimation函數(有finish屬性的才是doAnimation函數)。由於緩存中帶有finish標記,動畫對象一創建就將調用anim.stop( true )
所有動畫直接到結束狀態。
jQuery.fn.extend( {

    /* #7949 jQuery.fn.stop 
     * 停止當前動畫
    ---------------------------------------------------------------------- */
    // 指定type,則該type   clearQueue gotoEnd
    // type無值,則"fx" clearQueue,所有type gotoEnd
    stop: function( type, clearQueue, gotoEnd ) {

        // 用於刪除"非doAnimation"動畫(沒有tick函數加入timers堆棧全局interval執行,而是直接執行的,上面有介紹)
        var stopQueue = function( hooks ) {
            var stop = hooks.stop;
            delete hooks.stop;
            stop( gotoEnd );
        };

        // 參數提前,type=false當做"fx"處理(不支持非隊列,不得不懷疑有可能是開發者的紕漏)
        if ( typeof type !== "string" ) {
            gotoEnd = clearQueue;
            clearQueue = type;
            type = undefined;
        }
        // type不可能為false(有些多餘)
        if ( clearQueue && type !== false ) {
            this.queue( type || "fx", [] );
        }

        // 遍歷元素
        return this.each( function() {
            var dequeue = true,
                // type只有undefined和字符串兩種可能
                index = type != null && type + "queueHooks",
                timers = jQuery.timers,
                data = jQuery._data( this );

            // 顯式指定了隊列,stop"非doAnimation"動畫,並刪除stop函數自身
            if ( index ) {
                if ( data[ index ] && data[ index ].stop ) {
                    stopQueue( data[ index ] );
                }

            // type為undefined,遍歷查找所有帶有stop方法的所有隊列的hook緩存屬性,並調用刪除
            // rrun = /queueHooks$/
            } else {
                for ( index in data ) {
                    if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
                        stopQueue( data[ index ] );
                    }
                }
            }

            // 對timers中全局interval正在進行的動畫,對該元素該隊列的執行stop(type為undefined則該元素的全部stop)
            for ( index = timers.length; index--; ) {
                if ( timers[ index ].elem === this &&
                    ( type == null || timers[ index ].queue === type ) ) {

                    // gotoEnd為true直接到最終狀態,為false停止在當前狀態
                    // gotoEnd為true,stop內部會調用run(1),並resolve觸發promise,從而執行complete函數,從而dequeue下一個動畫(Animation處會詳細講)
                    // gotoEnd為false,就不會自動dequeue了,需要下面手動dequeue到下一個
                    timers[ index ].anim.stop( gotoEnd );
                    dequeue = false;
                    timers.splice( index, 1 );
                }
            }

            // 後續的動畫繼續進行,如果還有並且沒被clearQueue的話
            // 只有經過了元素動畫stop的過程,且gotoEnd為true(內部dequeue過)才不需要手動dequeue
            // "非doAnimation"動畫也是需要手動dequeue的
            if ( dequeue || !gotoEnd ) {
                jQuery.dequeue( this, type );
            }
        } );
    },

    /* #8001 jQuery.fn.finish
     * 當前
    ---------------------------------------------------------------------- */   
    finish: function( type ) {

        // undefined/null變為"fx",false仍然是false
        if ( type !== false ) {
            type = type || "fx";
        }
        return this.each( function() {
            var index,
                data = jQuery._data( this ),
                // 先拿到隊列堆棧,因為下面隊列緩存將替換為[]
                queue = data[ type + "queue" ],
                hooks = data[ type + "queueHooks" ],
                timers = jQuery.timers,
                length = queue ? queue.length : 0;

            // 標記為finish階段,此時所有的doAnimation執行時都會立即調用anim.stop(true),直接到動畫結束的樣子
            // 注意:由於js是單線程的,雖然這裡data與哪個隊列是無關的,看似其他type也被影響,但其實即使全局interval的tick也必須等該函數執行完,那時data.finsh已經不在了
            data.finish = true;

            // 清空queue,這樣下面的
            jQuery.queue( this, type, [] );

            // stop掉type對應的"非doAnimation"動畫
            if ( hooks && hooks.stop ) {
                hooks.stop.call( this, true );
            }

            // 正在執行的動畫anim.stop(true)直接到最終狀態
            for ( index = timers.length; index--; ) {

                // type為false的非隊列,也支持判斷
                if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
                    timers[ index ].anim.stop( true );
                    timers.splice( index, 1 );
                }
            }

            // 原來隊列裡的doAnimation函數遍歷執行,data.finish為true,因此都會直接到運動結束狀態
            for ( index = 0; index < length; index++ ) {
                // "非doAnimation"沒有finish屬性,該屬性指向自身
                if ( queue[ index ] && queue[ index ].finish ) {
                    queue[ index ].finish.call( this );
                }
            }

            // 刪除data.finsh標記
            delete data.finish;
        } );
    }
} );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127

Animation動畫

jQuery動畫的核心邏輯就是Animation( elem, properties, options ),立即開始一個動畫,把每步動畫tick推入全局interval調用堆棧jQuery.timers,返回一個animation對象(也是promise對象,通過上面的stop方法來實現stop、finish的終止動畫操作)。
tick函數是對properties中多屬性執行動畫。jq用面向對象的思想,把每個屬性的作為一個運動對象tween,把他們依次放入animation.tweens中(一個堆棧[]),使邏輯更分明。Animation內通過時間換算出百分比percent,然後傳入tween.run()來完成計算與設置。

Tween

Tween( elem, options, prop, end, easing )函數的構造和jq一樣,另Tween.prototype.init.prototype = Tween.prototype,從而Tween()返回一個實例並能夠使用原型方法cur、run。cur負責計算當前屬性值,run需傳入百分比,然後設置到對應的位置。duration是tweens中的tween公用,每步運動的百分比一致,在Animation的tick函數中處理。
每個屬性運動的easing是可以不同的,options.easing可以定義公用樣式,但優先級是低於options.specialEasing.prop這樣對屬性直接指定的,每個屬性的easing屬性可能不一樣。options對象也會被傳入,可以通過指定options.step函數,每個屬性的tween調用都會執行一次,this指定為elem,傳入參數now、tween。
cur和run中使用了Tween.propHooks[prop].set/get鉤子。鉤子代表例外,Tween.propHooks._default.get/set(tween)是標準的處理。scrollTop/scrollLeft有set鉤子。對於通常使用動畫的屬性,非特殊需求需要鉤子的確實幾乎沒有。
/* #7384 jQuery.Tween === Tween
 * 生成單個屬性的運動對象
 * Tween.prototype.init.prototype = Tween.prototype;
 * jQuery.fx = Tween.prototype.init;
---------------------------------------------------------------------- */   
function Tween( elem, options, prop, end, easing ) {
    return new Tween.prototype.init( elem, options, prop, end, easing );
}
jQuery.Tween = Tween;

Tween.prototype = {
    constructor: Tween,
    // 初始化
    init: function( elem, options, prop, end, easing, unit ) {
        this.elem = elem;
        this.prop = prop;
        // 默認"swing"
        this.easing = easing || jQuery.easing._default;
        this.options = options;
        // 初始化時設置start,now與start相等
        this.start = this.now = this.cur();
        this.end = end;
        // 除了cssNumber中指定的可以為數字的屬性,其它默認單位為px
        this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
    },
    // 計算當前樣式值
    cur: function() {
        // 首先看是否有鉤子
        var hooks = Tween.propHooks[ this.prop ];

        return hooks && hooks.get ?
            // 鉤子有get方法
            hooks.get( this ) :
            // 默認處理
            Tween.propHooks._default.get( this );
    },
    run: function( percent ) {
        var eased,
            // 鉤子
            hooks = Tween.propHooks[ this.prop ];


        if ( this.options.duration ) {
            // 時間過了百分之x,並不代表需要運動百分之x的距離,調用easing對應的函數
            // 可以在jQuery.easing中擴展運動函數,默認"swing"緩衝
            this.pos = eased = jQuery.easing[ this.easing ](
                percent, this.options.duration * percent, 0, 1, this.options.duration
            );
        } else {
            // duration為0,則percent一定為1,見tick函數中的計算
            this.pos = eased = percent;
        }
        // 計算當前應該運動到的值
        this.now = ( this.end - this.start ) * eased + this.start;

        // options對象可以指定step函數,每個tween調用一次,都會被執行
        if ( this.options.step ) {
            this.options.step.call( this.elem, this.now, this );
        }

        if ( hooks && hooks.set ) {
            // 鉤子
            hooks.set( this );
        } else {
            // 默認
            Tween.propHooks._default.set( this );
        }
        return this;
    }
};

Tween.prototype.init.prototype = Tween.prototype;

Tween.propHooks = {
    _default: {
        get: function( tween ) {
            var result;

            // 非dom節點或者屬性有值而style上無值的dom節點,均獲取屬性值返回
            // 注意:此處獲取的值是帶單位的
            if ( tween.elem.nodeType !== 1 ||
                tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) {
                return tween.elem[ tween.prop ];
            }

            // 獲取起作用的prop屬性樣式值去掉單位。對於不可parseFloat的字符串則直接返回
            result = jQuery.css( tween.elem, tween.prop, "" );

            // ""、null、undefined、"auto"都按照0返回。此處值無單位
            return !result || result === "auto" ? 0 : result;
        },
        set: function( tween ) {

            // use step hook for back compat - use cssHook if its there - use .style if its
            // available and use plain properties where available

            // 可以自己在jQuery.fx.step中添加鉤子,jq庫中沒有相關處理,是空對象{}
            if ( jQuery.fx.step[ tween.prop ] ) {
                jQuery.fx.step[ tween.prop ]( tween );

            // 凡是執行run的,之前一定執行過cur,調用默認get時,若執行了jQuery.css()則會把屬性修正後的字符串緩存在jQuery.cssProps中,這說明elem.style[修正屬性]一定存在,至少返回""
            // 在css樣式機制的通用鉤子cssHooks中的屬性,也說明一定可以通過$.style設置
            } else if ( tween.elem.nodeType === 1 &&
                ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null ||
                    jQuery.cssHooks[ tween.prop ] ) ) {
                // 默認獲取的樣式值(除了屬性上直接獲取的)不帶單位,所以加上unit設置
                jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );

            // 通常對於非節點、get使用鉤子的、get直接返回elem上屬性的情況,都直接設置在屬性上
            } else {
                tween.elem[ tween.prop ] = tween.now;
            }
        }
    }
};

// Support: IE <=9
// Panic based approach to setting things on disconnected nodes

Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
    set: function( tween ) {
        // 節點類型,並且有父節點(根元素也有父節點,為document)
        // 由於直接在屬性上獲取的值是帶單位的,因此直接設置
        if ( tween.elem.nodeType && tween.elem.parentNode ) {
            tween.elem[ tween.prop ] = tween.now;
        }
    }
};

jQuery.easing = {
    // 線性運動
    linear: function( p ) {
        return p;
    },
    // 緩衝
    swing: function( p ) {
        return 0.5 - Math.cos( p * Math.PI ) / 2;
    },
    _default: "swing"
};

jQuery.fx = Tween.prototype.init;

// Back Compat <1.8 extension point
jQuery.fx.step = {};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
創建tween對象,使用createTween( value, prop, animation )方法。內部會遍歷jQuery.tweeners[「*」]中的函數,默認只有一個函數,調用animation.createTween( prop, value ),核心是調用Tween()。
value支持累加值」+=300」、」+=300px」,普通使用帶不帶單位均可,因為addjustCSS會對tween.start/end進行處理,同一單位,並且轉換為數值,單位存在tween.unit上
/* #7536 createTween
 * 遍歷Animation.tweeners堆棧
---------------------------------------------------------------------- */   
function createTween( value, prop, animation ) {
    var tween,
        collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ),
        index = 0,
        length = collection.length;
    for ( ; index < length; index++ ) {
        if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) {

            // 有返回值,則返回,不再遍歷
            return tween;
        }
    }
}

/* #7848 jQuery.Animation.tweeners/tweener()
 * 創建tween對象,並加入animations.tweens堆棧
---------------------------------------------------------------------- */   
jQuery.Animation = jQuery.extend( Animation, {
    // createTween調用tweeners["*"]
    tweeners: {
        "*": [ function( prop, value ) {
            // Animation中animation的方法,創建一個tween對象,value為end值,可為'+=300'這樣的累加值
            var tween = this.createTween( prop, value );
            // adjustCSS可以把tween.end修正為數值(所以我們動畫指定單位與否都可以,還可用累加值),把單位放在tween.unit
            // adjustCSS可以把初始值和累加值的單位換算成一樣的,正確累加(詳細見css樣式機制講解)
            adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween );
            return tween;
        } ]
    },

    // 可以自己通過插件擴展tweeners,props可以把"ss * sd"變成["ss","*","sd"],對其中每個屬性對應的堆棧推入callback在棧頂
    tweener: function( props, callback ) {
        if ( jQuery.isFunction( props ) ) {
            // 參數提前
            callback = props;
            props = [ "*" ];
        } else {
            props = props.match( rnotwhite );
        }

        var prop,
            index = 0,
            length = props.length;

        for ( ; index < length ; index++ ) {
            prop = props[ index ];
            // 若對應屬性無堆棧,創建一個空的
            Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || [];
            // 把callback推入棧頂
            Animation.tweeners[ prop ].unshift( callback );
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

Animation

上面介紹到Animation返回一個promise對象,有什麼意義呢?在jQuery.speed中封裝的options.complete函數(可以調用dequeue),需要動畫結束時觸發,如果把它綁定在promise對象上,tick函數運動完畢調用resolve,即可觸發complete執行下一個doAnimation。
Animation中,在執行動畫前需要進行修正(即先刪除,再添加修正屬性和值)。
1、propFilter( props, animation.opts.specialEasing ):屬性修正。屬性變為小駝峰,把還會把margin、padding、borderWidth拆分成4個方向
/* #7311 jQuery.cssHooks.margin/padding/border
 * 鉤子,擴展屬性為四個方向的值
---------------------------------------------------------------------- */
// These hooks are used by animate to expand properties
jQuery.each( {
    margin: "",
    padding: "",
    border: "Width"
}, function( prefix, suffix ) {
    jQuery.cssHooks[ prefix + suffix ] = {
        expand: function( value ) {
            var i = 0,
                expanded = {},

                // "5px 3px" -> ['5px', '3px']
                parts = typeof value === "string" ? value.split( " " ) : [ value ];

            for ( ; i < 4; i++ ) {
                // cssExpand = [ "Top", "Right", "Bottom", "Left"]
                // 當parts只有一個值,四個值都為parts[0]
                // 當parts有兩個值,Bottom為parts[0=2-2],left為parts[1=3-2]
                // 當parts有三個值,left為parts[1=3-2]
                expanded[ prefix + cssExpand[ i ] + suffix ] =
                    parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
            }

            // 返回如{marginTop: 1px, marginRight: 2px, marginBottom: 1px, marginLeft: 2px}
            return expanded;
        }
    };

    // css機制中的,border、padding不能為負值,調用setPositiveNumber調整
    if ( !rmargin.test( prefix ) ) {
        jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
    }
} );

/* #7695 propFilter
 * 屬性修正。小駝峰 + expand屬性擴展
---------------------------------------------------------------------- */
function propFilter( props, specialEasing ) {
    var index, name, easing, value, hooks;

    for ( index in props ) {
        // 小駝峰
        name = jQuery.camelCase( index );

        // 此處與easing = value[ 1 ]、specialEasing[ name ] = easing共同修正了specialEasing[ name ]
        // easing優先級:value[ 1 ] > options.specialEasing[ name ] > options.easing
        easing = specialEasing[ name ];
        value = props[ index ];
        if ( jQuery.isArray( value ) ) {
            // 值可為數組,第2項指定easing,優先級最高(高於specialEasing)
            easing = value[ 1 ];
            // 此時,第1項為值
            value = props[ index ] = value[ 0 ];
        }

        // 屬性被修正,則修改屬性名,屬性值不變
        if ( index !== name ) {
            props[ name ] = value;
            delete props[ index ];
        }

        // expand擴展,margin/padding/border擴展為四個方向名值對形式
        hooks = jQuery.cssHooks[ name ];
        if ( hooks && "expand" in hooks ) {
            value = hooks.expand( value );
            // 刪除原有margin/padding/border屬性
            delete props[ name ];

            // 若已經單獨指定了。如"marginRight",優先級更高,不要修改它
            for ( index in value ) {
                if ( !( index in props ) ) {
                    props[ index ] = value[ index ];
                    specialEasing[ index ] = easing;
                }
            }
        } else {
            specialEasing[ name ] = easing;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
2、prefilters隊列:默認只有defaultPrefilter( elem, props, opts ),有四個用途
  • 1、當有非隊列動畫執行時,會啟動計數,只要有非隊列動畫沒結束,」fx」動畫隊列的queuehook自毀函數無法順利執行,會等全部結束才執行(用意暫時不明)
  • 2、若涉及到height/width的動畫,overflow先設置為hidden,動畫結束改回。通過給animation綁定alway函數實現(stop(false)會觸發reject,也需要改回,所以不綁定done函數)
  • 3、對於inline元素涉及到height/width的動畫,需要設置為」inline-block」,jq中的設置時display為none的也要變為顯示運動(這點挺奇怪,因為默認block的塊級如果是none就不會變為顯示)。但對於都是toggle/show/hide設置,但是全部都被過濾的,因為沒有動畫,需要還原為none
  • 4、支持屬性值為 「toggle」、」show」、」hide」。會被修正為適當的值。
toggle/show/hide動畫機制
使用時自覺遵守,一個動畫的屬性對象裡只能出現3者中的1種!!
當帶有toggle/show/hide的動畫單獨執行或異步執行時:
  • 1、先判斷isHidden,即是否隱藏(display:none)
  • 2、隱藏時調用hide無作用(過濾掉),顯示時調用show無作用(過濾掉)
  • 3、hide表示把元素prop屬性的值從now運動到0,運動完後調用jQuery( elem ).hide()變為不可見(原理是內部display設為none),但是要把屬性值還原為now。
  • 4、show表示把元素prop屬性的值從0運動到now,運動前把不可見狀態通過jQuery( elem ).show()變為可見
  • 5、toggle需要判斷當前是否隱藏,當前隱藏調用show,當前顯示調用hide
難點在於帶有toggle/show/hide的動畫同步執行時(同步指的是相同屬性有正在發生的動畫,不同屬性之間按上面規則進行):
  • 1、對於同步中排在第一個調用的,完全按照上面的規則
  • 2、從上面規則看出,無論show、hide、toggle,運動過程中都是顯示狀態(isHidden=false)
  • 3、既然運動中都是顯示狀態,異步時的第2條對同步的動畫(非第一個調用的)不約束。
  • 4、第一個動畫執行前會把屬性當前值now緩存到jQuery._data( elem, 「fxshow」),查看是否有該屬性緩存值來判斷誰是同步的動畫(即非第一個)
  • 5、對於非第一個的同步動畫,不以自身當前位置為參照,把緩存裡存的now(即第一個運動前的位置)當做hide的運動起點或show的運動終點
  • 6、toggle與show和hide不同,運動到相反而不是特定的狀態。當遇到toggle,需要緩存一個要運動到的終點狀態,運動結束立即刪除(例如:show->hide則緩存hide,沒執行完時同步調用toggle會查看緩存值,從而知道當前運動終點是hide->show)
  • 7、show、hide判斷是否同步必須相同elem的相同屬性。toggle判斷同步則是針對元素的狀態。toggle判斷無緩存,表示異步調用中,但是也可能是當前正在show、hide。由於show、hide的運動過程中都會為顯示狀態(可能同時有很多,既有show也有hide,duration也不同),因此未查找到toggle記錄的緩存時,統一是運動到隱藏show->hide。
jQuery小bug:
if ( value === 「show」 && dataShow && dataShow[ prop ] !== undefined ) { hidden = true; }之所以需要修改hidden,因為同步的show按照show->hide處理,後面的處理邏輯需要判斷hidden。但是遍歷屬性時,對於第一個動畫的屬性,若為show,變為hidden之前遍歷的不被處理,之後的都將從show->hide,與之前不一致。可以增加一個變量來輔助過濾那些屬性。
/* #7695 defaultPrefilter
 * inline修正、toggle/show/hide修正
---------------------------------------------------------------------- */
function defaultPrefilter( elem, props, opts ) {

    var prop, value, toggle, tween, hooks, oldfire, display, checkDisplay,
        anim = this,
        orig = {},
        style = elem.style,
        // 當前是否隱藏
        hidden = elem.nodeType && isHidden( elem ),
        dataShow = jQuery._data( elem, "fxshow" );

    // 非隊列情況,unqueued計數
    if ( !opts.queue ) {
        hooks = jQuery._queueHooks( elem, "fx" );
        if ( hooks.unqueued == null ) {
            hooks.unqueued = 0;
            oldfire = hooks.empty.fire;
            hooks.empty.fire = function() {
                // 非隊列動畫未完畢,"fx"堆棧和鉤子無法自毀
                if ( !hooks.unqueued ) {
                    oldfire();
                }
            };
        }
        hooks.unqueued++;

        // 不僅是done,動畫被中斷停止在當前位置觸發reject時,依然需要消減計數
        anim.always( function() {

            // deferred對象是遞延的,再套一層anim.always()與否不影響執行。但套一層會影響執行的順序,會添加到堆棧末尾
            anim.always( function() {
                hooks.unqueued--;
                if ( !jQuery.queue( elem, "fx" ).length ) {
                    hooks.empty.fire();
                }
            } );
        } );
    }

    // height/width動畫對overflow修正 + inline元素修正(長寬需inline-block才有效)
    if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) {

        // 記錄overflow狀態
        opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];

        // Set display property to inline-block for height/width
        // animations on inline elements that are having width/height animated
        display = jQuery.css( elem, "display" );

        // Test default display if display is currently "none"
        checkDisplay = display === "none" ?
            jQuery._data( elem, "olddisplay" ) || defaultDisplay( elem.nodeName ) : display;

        // 當前為inline、或者當前隱藏曾經為inline
        if ( checkDisplay === "inline" && jQuery.css( elem, "float" ) === "none" ) {

            // inline-level elements accept inline-block;
            // block-level elements need to be inline with layout
            if ( !support.inlineBlockNeedsLayout || defaultDisplay( elem.nodeName ) === "inline" ) {
                // 所有的情況都變為inline-block
                // 除了display為none,動畫全部是toggle/show/hide屬性,但沒有一個有效被過濾,無動畫,需要還原為none
                style.display = "inline-block";
            } else {
                // 低版本IE
                style.zoom = 1;
            }
        }
    }

    // 把overflow改為hidden
    if ( opts.overflow ) {
        style.overflow = "hidden";
        if ( !support.shrinkWrapBlocks() ) {
            // 運動無論是否成功結束,最後一定要吧overhidden改回來
            anim.always( function() {
                style.overflow = opts.overflow[ 0 ];
                style.overflowX = opts.overflow[ 1 ];
                style.overflowY = opts.overflow[ 2 ];
            } );
        }
    }

    // show/hide pass
    for ( prop in props ) {
        value = props[ prop ];

        // rfxtypes = /^(?:toggle|show|hide)$/
        if ( rfxtypes.exec( value ) ) {
            // 過濾屬性,異步時同狀態屬性動畫無作用。有作用的會加入orig[ prop ]
            delete props[ prop ];
            toggle = toggle || value === "toggle";
            if ( value === ( hidden ? "hide" : "show" ) ) {

                // 同步狀態調用show,按照hide->show處理。修正顯示狀態為hidden=true
                if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) {
                    hidden = true;
                } else {
                    // 過濾掉,異步同狀態
                    continue;
                }
            }
            // 記錄show的運動終點值,或hide的運動初始值
            orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );

        // 有效屬性時,不需要結尾的修正
        } else {
            display = undefined;
        }
    }

    // 進入toggle/show/hide屬性修正
    if ( !jQuery.isEmptyObject( orig ) ) {
        // 同步時
        if ( dataShow ) {
            // 有同步的toggle
            if ( "hidden" in dataShow ) {
                // 以緩存記錄作為當前狀態的依據
                hidden = dataShow.hidden;
            }
        } else {
            // elem的第一個動畫,為elem加上緩存
            dataShow = jQuery._data( elem, "fxshow", {} );
        }

        // 當前toggle執行完會變為的狀態,緩存起來
        if ( toggle ) {
            dataShow.hidden = !hidden;
        }
        // 對於hide->show的元素,先變為顯示狀態(否則從0到now的運動看不見)
        if ( hidden ) {
            jQuery( elem ).show();
        } else {
            // 對於show->hide的,結束時需要隱藏
            anim.done( function() {
                jQuery( elem ).hide();
            } );
        }
        // 順利結束則清緩存,並還原位置。中途中斷在當前位置的,為了後續動畫能還原,保留緩存中的now值
        anim.done( function() {
            var prop;
            jQuery._removeData( elem, "fxshow" );
            // 還原初始位置。對於show->hide的有意義,在運動到0後,變為隱藏狀態,並把值變為初始值
            for ( prop in orig ) {
                jQuery.style( elem, prop, orig[ prop ] );
            }
        } );

        // 創建toggle/show/hide屬性運動的tween對象
        for ( prop in orig ) {
            // 對於hide->show的,0(第一個動畫為0,同步的為當前值)->now(第一個動畫為now,同步為緩存); 對於show->hide,now->0
            tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );

            // hide->show,第一個動畫初始值調整為0,終點調整為當前值
            if ( !( prop in dataShow ) ) {
                // 第一個動畫,無論哪種情況,都要緩存now
                dataShow[ prop ] = tween.start;
                if ( hidden ) {
                    tween.end = tween.start;
                    // 從0開始,寬高從1開始
                    tween.start = prop === "width" || prop === "height" ? 1 : 0;
                }
            }
        }

    // display為none的inline元素,並且沒有生效的動畫屬性,改回none
    } else if ( ( display === "none" ? defaultDisplay( elem.nodeName ) : display ) === "inline" ) {
        style.display = display;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
最後是核心部分代碼,Animation( elem, properties, options )
/* #7732 Animation
 * 動畫核心,返回animation
---------------------------------------------------------------------- */
function Animation( elem, properties, options ) {
    var result,
        stopped,
        index = 0,
        length = Animation.prefilters.length,
        // 用於返回的animation對象對應的promise
        deferred = jQuery.Deferred().always( function() {

            // don't match elem in the :animated selector
            // 運動完或被stop後刪除tick.elem的引用
            delete tick.elem;
        } ),
        tick = function() {
            if ( stopped ) {
                return false;
            }
            var currentTime = fxNow || createFxNow(),
                // 還剩多長時間結束,時間過了,則為0,而不是負數
                remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),

                // 還剩百分之多少時間
                temp = remaining / animation.duration || 0,
                // 經過了百分之多少時間
                percent = 1 - temp,
                index = 0,
                length = animation.tweens.length;

            for ( ; index < length ; index++ ) {
                // 傳入百分比,把元素設置到合適位置
                animation.tweens[ index ].run( percent );
            }

            // tick函數每調用一次,options.progress就執行一次
            deferred.notifyWith( elem, [ animation, percent, remaining ] );

            // 返回剩餘時間,結束了則返回false(全局jQuery.fx.tick遍歷時以此判斷動畫是否結束,結束了就棧出)
            // 中途中斷的不是在這裡被resolve,而是在stop中,也有resolve的邏輯(見下方)
            if ( percent < 1 && length ) {
                return remaining;
            } else {
                // 觸發成功狀態,會調用complete,和defaultPrefilter中綁定的回調還原元素狀態
                deferred.resolveWith( elem, [ animation ] );
                return false;
            }
        },
        // 把對象中屬性和值copy到deferred.promise中得到animation(一個promise對象)
        animation = deferred.promise( {
            elem: elem,
            props: jQuery.extend( {}, properties ),
            // 深拷貝
            opts: jQuery.extend( true, {
                specialEasing: {},
                easing: jQuery.easing._default
            }, options ),
            originalProperties: properties,
            originalOptions: options,
            startTime: fxNow || createFxNow(),
            duration: options.duration,
            // tween隊列
            tweens: [],
            // 創建tween對象的函數,此處end不會被修正為數值(在Animation.tweeners["*"]中完成修正)
            createTween: function( prop, end ) {
                var tween = jQuery.Tween( elem, animation.opts, prop, end,
                        animation.opts.specialEasing[ prop ] || animation.opts.easing );
                // 推入tweens堆棧
                animation.tweens.push( tween );
                return tween;
            },
            // 用於外部來停止動畫的函數
            stop: function( gotoEnd ) {
                var index = 0,

                    // 如果在當前位置停止,length變為0
                    length = gotoEnd ? animation.tweens.length : 0;

                // 動畫已經被停止,返回
                if ( stopped ) {
                    return this;
                }
                // 標記stopped
                stopped = true;

                // gotoEnd為true,直接run(1);gotoEnd為false,length被設為0,不進行run
                for ( ; index < length ; index++ ) {
                    // 直接運動到結尾
                    animation.tweens[ index ].run( 1 );
                }

                // true,則觸發resolve成功
                if ( gotoEnd ) {
                    deferred.notifyWith( elem, [ animation, 1, 0 ] );
                    deferred.resolveWith( elem, [ animation, gotoEnd ] );
                } else {
                    // 觸發失敗,不會調用complete,在stop函數停止時,會顯示的調用dequeue
                    deferred.rejectWith( elem, [ animation, gotoEnd ] );
                }
                return this;
            }
        } ),
        props = animation.props;

    // 屬性修正,expand修正
    propFilter( props, animation.opts.specialEasing );

    for ( ; index < length ; index++ ) {
        // 默認只有一項defalutPrefilter,show/hide/toggle機制處理、inline元素處理。無返回值
        // 這裡指的是如果自己通過jQuery.tweener()進行了拓展hook
        result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts );

        // 默認不走這裡
        if ( result ) {
            if ( jQuery.isFunction( result.stop ) ) {
                // 與前面提到的"非doAnimation"動畫一樣,在hook.stop上添加阻止的函數(result.stop)
                jQuery._queueHooks( animation.elem, animation.opts.queue ).stop =
                    // result.stop.bind(result)
                    jQuery.proxy( result.stop, result );
            }
            // 返回,不再生成標準的Animation動畫
            return result;
        }
    }

    // 對每個屬性,生成tween加入tweens堆棧
    // createTween( props[prop], prop, animation )
    jQuery.map( props, createTween, animation );

    // 可以通過options.start指定動畫開始前調用的函數(如果需要的話)
    if ( jQuery.isFunction( animation.opts.start ) ) {
        animation.opts.start.call( elem, animation );
    }

    jQuery.fx.timer(
        // tick函數加入全局interval堆棧
        jQuery.extend( tick, {
            elem: elem,
            anim: animation,
            queue: animation.opts.queue
        } )
    );

    // 鏈式返回animation,從這裡也可以看出options還可以指定progress、done、complete、fail、always函數
    return animation.progress( animation.opts.progress )
        .done( animation.opts.done, animation.opts.complete )
        .fail( animation.opts.fail )
        .always( animation.opts.always );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149

toggle/show/hide

jq中提供了幾種便捷的show/hide/toggle動畫封裝。(原理見上小節」toggle/show/hide動畫機制」)
genFx( type, includeWidth ):type可為show/hide/toggle,將轉換為屬性對象。includeWidth指定是否包含寬度方面動畫變化。
genFx( name, true ) -> { height: name, width: name, opacity: name, marginTop/Right/Bottom/Left: name, paddingTop/Right/Bottom/Left: name }
genFx( name ) -> { height: name, marginTop/bottom: name, paddingTop/bottom: name }
/* #7516 genFx
 * show/hide/toggle動畫屬性對象轉換
---------------------------------------------------------------------- */
// includeWidth為true,是四向漸變
// includeWidth為false,是上下展開不漸變(透明度不變化)
function genFx( type, includeWidth ) {
    var which,
        attrs = { height: type },
        i = 0;

    // if we include width, step value is 1 to do all cssExpand values,
    // if we don't include width, step value is 2 to skip over Left and Right
    includeWidth = includeWidth ? 1 : 0;
    for ( ; i < 4 ; i += 2 - includeWidth ) {
        // cssExpand = [ "Top", "Right", "Bottom", "Left"]
        // 0 2 對應"Top" "Bottom",0 1 2 3全部都有
        which = cssExpand[ i ];
        attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
    }

    if ( includeWidth ) {
        // 透明度,寬度
        attrs.opacity = attrs.width = type;
    }

    return attrs;
}

/* #7921 jQuery.fn.fadeTo
 * 漸變,從0到to,不可見的也將可見
---------------------------------------------------------------------- */   
jQuery.fn.fadeTo = function( speed, to, easing, callback ) {

    // 把所有隱藏元素的設為顯示,並且透明度設為0(暫時看不見)
    return this.filter( isHidden ).css( "opacity", 0 ).show()

        // 回到this,所有元素opacity運動到to
        .end().animate( { opacity: to }, speed, easing, callback );
};

/* #8044 jQuery.fn.toggle/show/hide
 * 增強了css機制的jQuery.fn.toggle/show/hide接口,提供了動畫功能
---------------------------------------------------------------------- */
jQuery.each( [ "toggle", "show", "hide" ], function( i, name ) {
    var cssFn = jQuery.fn[ name ];
    jQuery.fn[ name ] = function( speed, easing, callback ) {
        // 無參數,或true、false則按照原有css機制觸發
        return speed == null || typeof speed === "boolean" ?
            cssFn.apply( this, arguments ) :
            // 四向漸變
            this.animate( genFx( name, true ), speed, easing, callback );
    };
} );

/* #8044 jQuery.fn.slideDown等
---------------------------------------------------------------------- */
jQuery.each( {
    slideDown: genFx( "show" ),  // 上下伸展不漸變
    slideUp: genFx( "hide" ),  // 上下回縮不漸變
    slideToggle: genFx( "toggle" ),  // 上下toggle不漸變
    fadeIn: { opacity: "show" },  // 四向漸變展開
    fadeOut: { opacity: "hide" },  // 四向漸變收縮
    fadeToggle: { opacity: "toggle" }  // 四向toggle漸變
}, function( name, props ) {
    jQuery.fn[ name ] = function( speed, easing, callback ) {
        return this.animate( props, speed, easing, callback );
    };
} );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
0
 
0

沒有留言:

張貼留言