闲话

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
// 计算次数,算出每次增量(与定时器的设置时间,严格相关)
times = duration / 20;
everyTimeAddNum = ( end - start ) / timers;// 方式 2
// 计算当前流逝的比例,根据比例设置最终值(有定时器即可,与定时时间无关)
passTime = ( +new Date() - createTime ) / duration;
passTime = passTime > 1 ? 1 : passTime;
toValue = ( end - start ) * passTime + start;

方式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掉全局定时器timerIdif ( !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 nothingif ( !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
};

同步、异步

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",无值则取默认400opt.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 ),// 参数修正到对象optalloptall = 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 ?// 直接遍历执行doAnimationthis.each( doAnimation ) :// 遍历元素把doAnimation加入对应元素的optall.queue队列this.queue( optall.queue, doAnimation );
};

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() {// 取消延迟调用nextwindow.clearTimeout( timeout );};} );
};

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 gotoEndstop: 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仍然是falseif ( 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;} );}
} );

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中指定的可以为数字的属性,其它默认单位为pxthis.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 nodesTween.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 = {};

创建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 );}}
}

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.easingeasing = 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;}}
}

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 animateddisplay = jQuery.css( elem, "display" );// Test default display if display is currently "none"checkDisplay = display === "none" ?jQuery._data( elem, "olddisplay" ) || defaultDisplay( elem.nodeName ) : display;// 当前为inline、或者当前隐藏曾经为inlineif ( checkDisplay === "inline" && jQuery.css( elem, "float" ) === "none" ) {// inline-level elements accept inline-block;// block-level elements need to be inline with layoutif ( !support.inlineBlockNeedsLayout || defaultDisplay( elem.nodeName ) === "inline" ) {// 所有的情况都变为inline-block// 除了display为none,动画全部是toggle/show/hide属性,但没有一个有效被过滤,无动画,需要还原为nonestyle.display = "inline-block";} else {// 低版本IEstyle.zoom = 1;}}}// 把overflow改为hiddenif ( 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 passfor ( 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=trueif ( 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 ) {// 有同步的toggleif ( "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->0tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );// hide->show,第一个动画初始值调整为0,终点调整为当前值if ( !( prop in dataShow ) ) {// 第一个动画,无论哪种情况,都要缓存nowdataShow[ 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;}
}

最后是核心部分代码,Animation( elem, properties, options )

/* #7732 Animation* 动画核心,返回animation
---------------------------------------------------------------------- */
function Animation( elem, properties, options ) {var result,stopped,index = 0,length = Animation.prefilters.length,// 用于返回的animation对象对应的promisedeferred = 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变为0length = gotoEnd ? animation.tweens.length : 0;// 动画已经被停止,返回if ( stopped ) {return this;}// 标记stoppedstopped = true;// gotoEnd为true,直接run(1);gotoEnd为false,length被设为0,不进行runfor ( ; 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函数停止时,会显示的调用dequeuedeferred.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()进行了拓展hookresult = 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 );
}

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 RightincludeWidth = 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 );};
} );

jQuery源码解析(5)—— Animation动画相关推荐

  1. jQuery 源码解析(三十一) 动画模块 便捷动画详解

    jquery在$.animate()这个接口上又封装了几个API,用于进行匹配元素的便捷动画,如下: $(selector).show(speed,easing,callback)        ;如 ...

  2. jQuery源码解析(架构与依赖模块)

    jQuery设计理念 引用百科的介绍: jQuery是继prototype之后又一个优秀的Javascript框架.它是轻量级的js库 ,它兼容CSS3,还兼容各种浏览器(IE 6.0+, FF 1. ...

  3. jQuery源码解析(架构与依赖模块)第一章 理解架构

    1-1 jQuery设计理念 引用百科的介绍: jQuery是继prototype之后又一个优秀的Javascript框架.它是轻量级的js库 ,它兼容CSS3,还兼容各种浏览器(IE 6.0+, F ...

  4. jquery源码解析:代码结构分析

    本系列是针对jquery2.0.3版本进行的讲解.此版本不支持IE8及以下版本. (function(){ (21, 94)     定义了一些变量和函数,   jQuery = function() ...

  5. JQuery 源码解析资料

    2019独角兽企业重金招聘Python工程师标准>>> jQuery源码分析系列目录 jQuery源码解读-理解架构 jQuery源码解析(架构与依赖模块) jQuery v1.10 ...

  6. jquery源码解析:jQuery数据缓存机制详解2

    上一课主要讲了jQuery中的缓存机制Data构造方法的源码解析,这一课主要讲jQuery是如何利用Data对象实现有关缓存机制的静态方法和实例方法的.我们接下来,来看这几个静态方法和实例方法的源码解 ...

  7. jQuery 源码解析一:jQuery 类库整体架构设计解析

    如果是做 web 的话,相信都要对 Dom 进行增删查改,那大家都或多或少接触到过 jQuery 类库,其最大特色就是强大的选择器,让开发者脱离原生 JS 一大堆 getElementById.get ...

  8. jQuery源码解析之on事件绑定

    本文采用的jQuery源码为jquery-3.2.1.js jquery的on方法用来在选定的元素上绑定一个或多个事件处理函数. 当参数selector存在时,通常会用来对已经存在的元素或将来即将添加 ...

  9. 浅谈jquery源码解析

    本文主要是针对jquery  v3.x版本来描述的,将从以下几个方面谈谈我对jquery的认识, 总体架构 $与$.fn jQuery.fn.init  (重要) jQuery.extend  与jQ ...

最新文章

  1. loadClass和forName 的区别
  2. 用STM32F103C8T6的做IAP时,在跳转时一直进入HardFault_Handler 解决方法
  3. c语言二极管控制程序,二极管(STC89C52): 编写程序控制二极管花样流水灯
  4. ***某知名网络安全公司
  5. Memcached 源码分析——从 main 函数说起
  6. cvpr2020 人脸检测与识别_CVPR 2020 | 元学习人脸识别框架详解
  7. 线性表_使用栈实现二进制转换到八进制/十进制/十六进制
  8. 快餐店选址指南--转
  9. 明解c语言入门篇第三版第九章答案,明解C语言 入门篇 第一章答案
  10. clustalX2使用以及相关的问题
  11. matlab三次方程求根,如何用matlab求一元三次方程的最小正根?
  12. 数据库系统—数据查询
  13. LeetCode.1033-移动石头直到连续(Moving Stones Until Consecutive)
  14. Zabbix5.0如何发送短信
  15. 使用Java自动化方法模拟Android手机点击、触屏事件
  16. linux桌面应用软件,ubuntu16.04-18.04 桌面应用软件推荐系列(一)
  17. 2019年电赛D题《简易电路特性测试仪》全过程
  18. @Primary 使用
  19. 再读新疆系列(九)——归意切切
  20. 2021-12-06 自动化专业C语言上机作业参考答案04

热门文章

  1. java与微信小程序通讯_java与微信小程序实现websocket长连接
  2. linux撤销编译,linux重新编译内核
  3. python flask高级编程之restful_flask-restful使用总结
  4. 一文带你读懂Self-Supervised Learning(自监督学习)
  5. C语言指针变量--图示易懂
  6. 机器学习(十一)谱聚类算法
  7. OpenCV属性页配置问题~
  8. 一阶低通滤波器方程_一阶RC低通滤波器杂记
  9. 树莓派zero+mysql_关于树莓派zero的系统安装配置部署
  10. c++第n小的质数_形形色色的素数 -- 质数定理