jQuery的动画机制有800行, 虽然不如样式的1300行,难度上却是不减。由于事前不了解animate接口的细节使用规则,看代码期间吃了很多苦头,尤其是深恶痛绝的defaultPrefilter函数,靠着猜想和数次的逐行攻略,终于全部拿下。本文将一点一点拆解出jq的动画机制及具体实现,解析各种做法的目的、解耦的方式、必要的结构、增强的辅助功能。
需要提前掌握queue队列的知识,css样式机制、data缓存、deferred对象至少要了解核心API的功能。有兴趣可以参考我之前的几篇分析。
(本文采用 1.12.0 版本进行讲解,用 #number 来标注行号)
jQuery的动画机制比较复杂,下面将逐一分析其中要点。
教学时常用的动画函数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掉全局定时器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
};
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 );
};
现在我们有了同步、异步两种方式,但在同步的时候,有可能出现重复触发某元素动画,而我们并不需要。在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 );
};
} );
};
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;
} );
}
} );
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( 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 = {};
创建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返回一个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;
}
}
}
2、prefilters队列:默认只有defaultPrefilter( elem, props, opts ),有四个用途
toggle/show/hide动画机制
使用时自觉遵守,一个动画的属性对象里只能出现3者中的1种!!
当带有toggle/show/hide的动画单独执行或异步执行时:
难点在于带有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;
}
}
最后是核心部分代码,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 );
}
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 );
};
} );