jQuery源码解析—— Animation动画
2017-02-14 18:12
211 查看
闲话
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); }1234567891011121312345678910111213[/code]如何计算每步运动的值需要讲究,举个栗子:
// 方式 1 // 计算次数,算出每次增量(与定时器的设置时间,严格相关) times = duration / 20; everyTimeAddNum = ( end - start ) / timers; // 方式 2 // 计算当前流逝的比例,根据比例设置最终值(有定时器即可,与定时时间无关) passTime = ( +new Date() - createTime ) / duration; passTime = passTime > 1 ? 1 : passTime; toValue = ( end - start ) * passTime + start;12345678910111234567891011[/code]方式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 };1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757612345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576[/code]
同步、异步
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 ); };123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475[/code]
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 ); }; } ); };123456789101112131415123456789101112131415[/code]
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; } ); } } );123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127[/code]
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 = {};123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145[/code]创建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 ); } } }12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455561234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556[/code]
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; } } }12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182831234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283[/code]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设置,但是全部都被过滤的,因为没有动画,需要还原为none4、支持属性值为 “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; } }123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171[/code]最后是核心部分代码,
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 ); }123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149[/code]
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 ); }; } );
来源网站:http://blog.csdn.net/vbdfforever/article/details/51121012
相关文章推荐
- Android Animation学习(五) ApiDemos解析:容器布局动画 LayoutTransition
- Android开发学习之Animation之Android帧动画解析
- android动画(Animation)解析
- Animation动画的解析
- Android Animation 动画解析
- Android 属性动画(Property Animation) 完全解析
- OSG动画库Animation解析(一)
- jQuery源码解析(5)—— Animation动画
- IOS CoreAnimation 核心动画解析
- Android Animation学习(三) ApiDemos解析:XML动画文件的使用
- Android 开关机动画 BootAnimation/ShutdownAnimation 解析
- Android Animation学习(四) ApiDemos解析:多属性动画
- Android Animation学习(五) ApiDemos解析:容器布局动画 LayoutTransition
- Android Animation学习(五) ApiDemos解析:容器布局动画 LayoutTransition
- iOS 动画Animation - 6 - 2:实战练习之淘宝购物车动画解析
- View 动画 Animation 运行原理解析
- 虾扯蛋:Android View动画 Animation不完全解析
- Animation动画的解析与自定义Animation
- 动画Animation 和 xml 解析Animation
- CSS3新增样式大解析:[8]animation之元素动画