首先需要有隊(duì)列(queue)的基本知識(shí)。見(jiàn)上一章。
相關(guān)教程:jQuery下的動(dòng)畫處理總結(jié): //www.survivalescaperooms.com/article/42000.htm
jQuery 1.9.1源碼分析系列(十五)動(dòng)畫處理之緩動(dòng)動(dòng)畫核心Tween ://www.survivalescaperooms.com/article/75821.htm
a.動(dòng)畫入口jQuery.fn.animate函數(shù)執(zhí)行流程詳解
--------------------------------------------------------------------------------
先根據(jù)參數(shù)調(diào)用jQuery.speed獲取動(dòng)畫相關(guān)參數(shù),得到一個(gè)類似如下的對(duì)象;并且生成動(dòng)畫執(zhí)行函數(shù)doAnimation
optall = { complete: fnction(){...},//動(dòng)畫執(zhí)行完成的回調(diào) duration: 400,//動(dòng)畫執(zhí)行時(shí)長(zhǎng) easing: "swing",//動(dòng)畫效果 queue: "fx",//動(dòng)畫隊(duì)列 old: false/fnction(){...},} var empty = jQuery.isEmptyObject( prop ), optall = jQuery.speed( speed, easing, callback ), doAnimation = function() { //在特征的副本上操作,保證每個(gè)特征效果不會(huì)被丟失 var anim = Animation( this, jQuery.extend( {}, prop ), optall ); doAnimation.finish = function() { anim.stop( true ); }; //空動(dòng)畫或完成需要立馬解決 if ( empty || jQuery._data( this, "finish" ) ) { anim.stop( true ); } };doAnimation.finish = doAnimation; 沒(méi)有動(dòng)畫正在執(zhí)行則馬上執(zhí)行動(dòng)畫,否則將動(dòng)畫壓入動(dòng)畫隊(duì)列等待執(zhí)行
//沒(méi)有動(dòng)畫在執(zhí)行則馬上執(zhí)行動(dòng)畫,否則將動(dòng)畫壓入動(dòng)畫隊(duì)列等待執(zhí)行return empty || optall.queue === false ? this.each( doAnimation ) : this.queue( optall.queue, doAnimation );
可以看出,真正執(zhí)行動(dòng)畫的地方是Animation( this, jQuery.extend( {}, prop ), optall )函數(shù)
b. jQuery內(nèi)部函數(shù)Animation詳解
--------------------------------------------------------------------------------
Animation ( elem, properties, options ). properties是要進(jìn)行動(dòng)畫的css特征,options是動(dòng)畫相關(guān)選項(xiàng){complete: function () {…},duration: 400,easing: undefined,old: false,queue: "fx"}。
首先,初始化一個(gè)延時(shí)對(duì)象,這個(gè)延時(shí)對(duì)象用來(lái)處理動(dòng)畫隊(duì)列。
deferred = jQuery.Deferred().always( function() { // don't match elem in the :animated selector delete tick.elem;}), 然后,生成一個(gè)每一個(gè)時(shí)間點(diǎn)(相鄰兩個(gè)時(shí)間點(diǎn)的事件間隔默認(rèn)為13毫秒)上都會(huì)執(zhí)行的函數(shù)tick,這個(gè)tick函數(shù)會(huì)保存在jQuery.timers中,然后每次執(zhí)行jQuery.fx.tick的時(shí)候會(huì)取出來(lái)執(zhí)行。
tick = function() { if ( stopped ) { return false; } var currentTime = fxNow || createFxNow(), remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497) temp = remaining / animation.duration || 0, percent = 1 - temp, index = 0, length = animation.tweens.length; //執(zhí)行動(dòng)畫效果 for ( ; index < length ; index++ ) { animation.tweens[ index ].run( percent ); } //生成進(jìn)度報(bào)告 deferred.notifyWith( elem, [ animation, percent, remaining ]); if ( percent < 1 && length ) { return remaining; } else { //動(dòng)畫執(zhí)行完畢,執(zhí)行所有延時(shí)隊(duì)列中的函數(shù)(包括清除動(dòng)畫相關(guān)的數(shù)據(jù)) deferred.resolveWith( elem, [ animation ] ); return false; }} 我們看到j(luò)Query對(duì)動(dòng)畫進(jìn)度的處理:
remaining = Math.max( 0, animation.startTime + animation.duration - currentTime )temp = remaining / animation.duration || 0,percent = 1 - temp,
進(jìn)度百分比 = 1 - 剩余時(shí)間百分比。
平常我們是這么處理:假設(shè)時(shí)間13毫秒執(zhí)行一次動(dòng)畫,當(dāng)前是第n此執(zhí)行,總的動(dòng)畫時(shí)長(zhǎng)為T。那么
進(jìn)度百分比 = (n*13)/T
實(shí)際上這種算法得到的時(shí)間n*13是不準(zhǔn)確的,因?yàn)閏pu不只是你一個(gè)程序在執(zhí)行,時(shí)間片分給你的時(shí)候往往都比n*13大。而且是一個(gè)很不準(zhǔn)確的值,導(dǎo)致動(dòng)畫感覺(jué)時(shí)快時(shí)慢,不連貫。而jQuery這種方式保證當(dāng)前的事件點(diǎn)上動(dòng)畫執(zhí)行結(jié)果的準(zhǔn)確性,畢竟事件是最新計(jì)算結(jié)果。
第三,生成動(dòng)畫用的所有特征組成的對(duì)象animation(這個(gè)對(duì)象結(jié)構(gòu)如源碼所示),animation.props中保存的是用戶傳入的特征(動(dòng)畫最終目標(biāo))。
animation = deferred.promise({ elem: elem, props: jQuery.extend( {}, properties ), opts: jQuery.extend( true, { specialEasing: {} }, options ), originalProperties: properties, originalOptions: options, startTime: fxNow || createFxNow(), duration: options.duration, tweens: [], createTween: function( prop, end ) { var tween = jQuery.Tween( elem, animation.opts, prop, end, animation.opts.specialEasing[ prop ] || animation.opts.easing ); animation.tweens.push( tween ); return tween; }, stop: function( gotoEnd ) { var index = 0, // if we are going to the end, we want to run all the tweens // otherwise we skip this part length = gotoEnd ? animation.tweens.length : 0; if ( stopped ) { return this; } stopped = true; for ( ; index < length ; index++ ) { animation.tweens[ index ].run( 1 ); } // resolve when we played the last frame // otherwise, reject if ( gotoEnd ) { deferred.resolveWith( elem, [ animation, gotoEnd ] ); } else { deferred.rejectWith( elem, [ animation, gotoEnd ] ); } return this; }}) 第四,調(diào)用propFilter修正css特征名稱以便能被瀏覽器識(shí)別,其中需要注意的是borderWidth/padding/margin指的不是一個(gè)css特征,而是四個(gè)(上下左右)
//經(jīng)過(guò)propFilter,animation.opts.specialEasing添加了相應(yīng)的特征propFilter( props, animation.opts.specialEasing );
舉例說(shuō)明propFilter修正成果。
例1,css特征{ height: 200 }的修正后結(jié)果為:
props = { height: 200 }animation.opts.specialEasing = {height: undefined} 例2:,css特征{margin:200}的修正結(jié)果為:
props = { marginBottom: 200,marginLeft: 200,marginRight: 200,marginTop: 200 }animation.opts.specialEasing = { marginBottom: undefined,marginLeft: undefined,marginRight: undefined,marginTop: undefined } 第五,調(diào)用defaultPrefilter做適配處理:比如對(duì)height/width的動(dòng)畫要求display和overflow為特定的值才能有效果;比如對(duì)show/hide動(dòng)畫需要對(duì)一大堆css特征值進(jìn)行動(dòng)畫,并且在函數(shù)里就調(diào)用createTweens生成緩動(dòng)動(dòng)畫。
// animationPrefilters[0] = defaultPrefilterfor ( ; index < length ; index++ ) { result = animationPrefilters[ index ].call( animation, elem, props, animation.opts ); if ( result ) { return result; }} 其中animationPrefilters[ index ]值得函數(shù)就是defaultPrefilter,defaultPrefilter函數(shù)處理有幾個(gè)比較重要的地方
defaultPrefilter重點(diǎn)1:內(nèi)聯(lián)元素中height/width相關(guān)動(dòng)畫需要設(shè)置display特征值為inline-block
// height/width overflow passif ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) { //確保沒(méi)有什么偷偷出來(lái) //記錄3個(gè)overflow相關(guān)特征,因?yàn)镮E不能改變overflow特征值, //當(dāng)overflowX和overflowY設(shè)置了相同的值 opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; // 內(nèi)聯(lián)元素中height/width相關(guān)動(dòng)畫需要設(shè)置display特征值為inline-block if ( jQuery.css( elem, "display" ) === "inline" && jQuery.css( elem, "float" ) === "none" ) { // 內(nèi)聯(lián)元素接受inline-block; // 塊級(jí)元素必須內(nèi)嵌在布局上 if ( !jQuery.support.inlineBlockNeedsLayout || css_defaultDisplay( elem.nodeName ) === "inline" ) { style.display = "inline-block"; } else { style.zoom = 1; } }} defaultPrefilter重點(diǎn)2:對(duì)于height/width動(dòng)畫overflow都要設(shè)置為"hidden",動(dòng)畫完成后恢復(fù)。這個(gè)有利于提高渲染速度。
//對(duì)于height/width動(dòng)畫overflow都要設(shè)置為"hidden",動(dòng)畫完成后恢復(fù)if ( opts.overflow ) { style.overflow = "hidden"; //收縮包裝塊 if ( !jQuery.support.shrinkWrapBlocks ) { anim.always(function() { style.overflow = opts.overflow[ 0 ]; style.overflowX = opts.overflow[ 1 ]; style.overflowY = opts.overflow[ 2 ]; }); }} defaultPrefilter重點(diǎn)3:show/hide動(dòng)畫的特殊處理:show/hide動(dòng)畫調(diào)用genFx得到形如
props = { height: "hide" marginBottom: "hide" marginLeft: "hide" marginRight: "hide" marginTop: "hide" opacity: "hide" paddingBottom: "hide" paddingLeft: "hide" paddingRight: "hide" paddingTop: "hide" width: "hide" } 需要進(jìn)行動(dòng)畫處理的特征壓入handled列表,并將相應(yīng)的特征刪除,后面會(huì)生成相應(yīng)的緩動(dòng)動(dòng)畫。
for ( index in props ) { value = props[ index ]; //rfxtypes = /^(?:toggle|show|hide)$/。可以看到最終只有和show/hide的動(dòng)畫才會(huì)被饒茹handled中 if ( rfxtypes.exec( value ) ) { delete props[ index ]; toggle = toggle || value === "toggle"; //如果當(dāng)前節(jié)點(diǎn)的狀態(tài)和指定的狀態(tài)相同則不需要處理直接進(jìn)行下一個(gè)狀態(tài)判斷 if ( value === ( hidden ? "hide" : "show" ) ) { continue; } handled.push( index ); }}//有需要執(zhí)行的動(dòng)畫處理則進(jìn)入分支,里面會(huì)對(duì)各個(gè)特征動(dòng)畫生成緩動(dòng)動(dòng)畫length = handled.length;if ( length ) { dataShow = jQuery._data( elem, "fxshow" ) || jQuery._data( elem, "fxshow", {} ); if ( "hidden" in dataShow ) { hidden = dataShow.hidden; } // toggle需要保存狀態(tài) - enables .stop().toggle() to "reverse" if ( toggle ) { dataShow.hidden = !hidden; } if ( hidden ) { jQuery( elem ).show(); } else { anim.done(function() { jQuery( elem ).hide(); }); } anim.done(function() { var prop; jQuery._removeData( elem, "fxshow" ); for ( prop in orig ) { jQuery.style( elem, prop, orig[ prop ] ); } }); for ( index = 0 ; index < length ; index++ ) { prop = handled[ index ]; //生成緩動(dòng)動(dòng)畫 tween = anim.createTween( prop, hidden ? dataShow[ prop ] : 0 ); orig[ prop ] = dataShow[ prop ] || jQuery.style( elem, prop ); if ( !( prop in dataShow ) ) { dataShow[ prop ] = tween.start; if ( hidden ) { tween.end = tween.start; tween.start = prop === "width" || prop === "height" ? 1 : 0; } } }} 第六,生成緩動(dòng)動(dòng)畫,show/hide在defaultPrefilter函數(shù)里面已經(jīng)處理(上面的源碼)。
createTweens( animation, props );
我們來(lái)看一看createTweens中具體做了什么,先看一下createTweens之前的animation對(duì)象

然后看一下經(jīng)過(guò)createTweens之后的animation對(duì)象的tweens數(shù)組變成了

將margin分解成了四個(gè)屬性(marginTop/Right/Bottom/Left)并且每個(gè)屬性都有自己的動(dòng)畫特征。
第七,啟動(dòng)動(dòng)畫計(jì)時(shí),定時(shí)執(zhí)行tick
//啟動(dòng)動(dòng)畫計(jì)時(shí)jQuery.fx.timer( jQuery.extend( tick, { elem: elem, anim: animation, queue: animation.opts.queue })); 最后,將傳入的動(dòng)畫結(jié)束回調(diào)加入延時(shí)隊(duì)列
//從options中獲取回調(diào)函數(shù)添加到延時(shí)隊(duì)列中return animation.progress( animation.opts.progress ) .done( animation.opts.done, animation.opts.complete ) .fail( animation.opts.fail ) .always( animation.opts.always );
Animation函數(shù)流程到此為止
拓展:
前面提到的genFx函數(shù)是專門用在toggle、hide、show時(shí)獲取相關(guān)的需要?jiǎng)赢嫷奶卣鞯?/p>
最終生成的attrs = { height: "show", marginTop: "show", marginRight: "show",//當(dāng)includeWidth為false時(shí)沒(méi)有 marginBottom: "show", marginLeft: "show",//當(dāng)includeWidth為false時(shí)沒(méi)有 opacity: "show", width: "show"} function genFx( type, includeWidth ) { var which, attrs = { height: type }, i = 0; //如果包括寬度,步長(zhǎng)值為1來(lái)完成所有cssExpand值, //如果不包括寬度,步長(zhǎng)值是2跳過(guò)左/右值 //cssExpand = [ "Top", "Right", "Bottom", "Left" ] includeWidth = includeWidth? 1 : 0; for( ; i < 4 ; i += 2 - includeWidth ) { which = cssExpand[ i ]; attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; } if ( includeWidth ) { attrs.opacity = attrs.width = type; } return attrs;} Animation函數(shù)比較復(fù)雜,童鞋們可以隨便使用例子去跟蹤代碼。這個(gè)是理解jQuery源碼的一種比較好的方式。推薦兩個(gè)例子:
第一個(gè),有hide/show的例子:$("#id").hide(1000);
第二個(gè),其他例子:$("#id").animate({"marginLeft":500},1000);
jQuery 1.9.1源碼分析系列(十五)之動(dòng)畫處理 的全部?jī)?nèi)容就給大家介紹到這里,有問(wèn)題隨時(shí)給我留言,謝謝。!
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注