jQuery源码分析系列(40): 动画设计
前言
jQuery动画是通过animate这个API设置执行的,其内部也是按照每一个animate的划分封装了各自动画组的行为,
包括数据过滤、缓动公式、一些动画默认参数的设置、元素状态的调整、事件的处理通知机制、执行等等
换句话说,我们可以把animate看作一个对象,对象封装自己的一系列属性与方法。
jQuery可以支持连续动画,那么animate与animate之间的切换就是通过队列.queue,这个之前就已经详细的解释过了
动画的参数
jQuery的内部的方法都是针对API的处理范围设计的
我们看看Animation方法的支持情况:
.animate( properties [, duration ] [, easing ] [, complete ] )
.animate( properties, options )
- 区别就与第二组数据的传递了,options是支持对象传参
- properties参数就是写一个CSS属性和值的对象,动画都是涉及变化的,那么什么值才能变化?
- 理论上来说有数值的属性都是可以变化的,
width
,height
或者left
可以执行动画,但是background-color
不能,但是也不是绝对的,主要看数据的解析度,可以用插件支持 - 除了样式属性, 一些非样式的属性,如
scrollTop
和scrollLeft
,以及自定义属性,也可应用于动画 - 除了定义数值,每个属性能使用
'show'
,'hide'
, 和'toggle'
。这些快捷方式允许定制隐藏和显示动画用来控制元素的显示或隐藏。为了使用jQuery内置的切换状态跟踪,'toggle'
关键字必须在动画开始前给定属性值
简单的来说,就是把一对的参数丢大animate方法里面,然后animate就开始执行你参数规定的动画了,
那么动画每执一次就会通过回调通知告诉开发者,具体有complete/done/fail/always/step接口等等
理解定义
<img id="book" alt="" width="100" height="123" style="background:red;opacity:1;position: relative; left: 500px;" /> book.animate({ opacity: 0.25, left: '50', height: 'toggle' }, { duration :1000, specialEasing: { height: 'linear' }, step: function(now, fx) { console.log('step') }, progress:function(){ console.log('progress') }, complete:function(){ console.log('动画完成') } })
首先,动画的参数都是最终值都是相对数据
如上img元素的起始
opacity是1,那么通过动画改成成0.25
left是500,那么通过动画改成成50
height为'toggle' 意味着如果是隐藏与显示的自动切换
step:是针对opacity/left/height各自动画,每次改变通知三次
progress 是把opacity/left/height看成一组了,每次改变只通知一次
动画的原理
jQuery动画的原理还是很简单的,靠定时器不断的改变元素的属性
我们模拟下animate的大致实现
让元素执行一个2秒的动画到坐标为left 50的区域
animate({
left: '50', duration: '2000' }
按照常规的思路,我们需要3个参数
- 动画开始位置
- 动画的结束位置
- 动画的运行时间
思路一:等值变化
我们在animate内部还需要计算出当然元素的初始化布局的位置(比如500px),那么我们在2秒的时间内需变换成50px,也就是运行的路劲长就是500-50 = 450px
那么算法是不是呼之欲出了?
每毫秒移动的距离 pos = 450/2000 = 0.225px
每毫秒移动left = 初始位置 (+/-) 每毫秒递增的距离(pos * 时间)
这样算法我们放到setInterval就会发现错的一塌糊涂,我们错最本质的东西:JS是单线程,定时器都是排队列的,理论上也达不到1ms绘制一次dom
所以每次产生的这个下一次绘制的时间差根本不是一个等比的,所以我们按照线性的等值递增是有误的
function animate(elem, options){ //动画初始值 var start = 500 //动画结束值 var end = options.left //动画id var timerId; var createTime = function(){ return (+new Date) } var startTime = createTime(); //需要执行动画的长度 var anminLength = start - end; //每13毫秒要跑的位置 var pos = anminLength/options.time * 13 var pre = start; var newValue; function tick(){ if(createTime() - startTime < options.time){ newValue = pre - pos //动画执行 elem.style['left'] = newValue + 'px'; pre = newValue }else{ //停止动画 clearInterval(timerId); timerId = null; console.log(newValue) } } //开始执行动画 var timerId = setInterval(tick, 13); }
思路一实现:
思路二:动态计算
setInterval的调用是不规律的,但是调用的时间是(2秒)是固定的,我们可以在每次调用的时候算法时间差的比值,用这个比值去计算移动的距离就比较准确了
remaining = Math.max(0, startTime + duration - currentTime),
通过这个公司我们计算出,每次setInterval调用的时候,当前时间在总时间中的一个位置
remaining
看到没有,这个值其实很符合定时器的特性,也是一个没有规律的值
根据这个值,我们可以得出当前位置的一个百分比了
var remaining = Math.max(0, startTime + options.duration - createTime()) var temp = remaining / options.duration || 0; var percent = 1 - temp;
pecent
那么这个移动的距离就很简单了
我把整个公式就直接列出来了
var createTime = function(){ return (+new Date) } //元素初始化位置 var startLeft = 500; //元素终点位置 var endLeft = 50; //动画运行时间 var duration = 2000; //动画开始时间 var startTime = createTime(); function tick(){ //每次变化的时间 var remaining = Math.max(0, startTime + duration - createTime()) var temp = remaining / duration || 0; var percent = 1 - temp; //最终每次移动的left距离 var leftPos = (endLeft- startLeft) * percent +startLeft; } //开始执行动画 setInterval(tick, 13);
leftPos就是每次移动的距离了,基本上比较准确了,事实上jQuery内部也就是这么干的
这里13代表了动画每秒运行帧数,默认是13毫秒。属性值越小,在速度较快的浏览器中(例如,Chrome),动画执行的越流畅,但是会影响程序的性能并且占用更多的 CPU 资源
在新的游览器中,我们都可以采用requestAnimationFrame更优
思路二实现:
动画的扩展
知道动画处理的基本原理与算法了,那么jQuery在这个基础上封装扩展,让动画使用起来更灵活方便
我归纳有几点:
- 参数的多形式传递
- 基于promise的事件反馈
- 增加属性的show/hide/toggle的快捷方式
- 可以给css属性设置独立的缓动函数
基于promise的事件通知
得益于deferred的机制,可以让一个对象转化成带有promise的特性,实现了done/fail/always/progress等等一系列的事件反馈接口
这样的设计我们并不陌生在ready、ajax包括动画都是基于这样的异步模型的结构
deferred = jQuery.Deferred() //生成一个动画对象了 animation = deferred.promise({}) //混入动画的属性与方法
那么这样操作的一个好处就是,可以把逻辑处理都放到一块
我们在代码的某一个环节针对特别的处理,需要临时改变一些东西,但是在之后我们希望又恢复原样,为了逻辑的清晰,我们可以引入deferred.alway方法
在某一个环节改了一个属性,然后注册到alway方法上一个完成的回调用来恢复,这样的逻辑块是很清晰的
style.overflow = "hidden"; anim.always(function() { //完成后恢复溢出 style.overflow = opts.overflow[0]; style.overflowX = opts.overflow[1]; style.overflowY = opts.overflow[2]; });
增加属性的show/hide/toggle的快捷方式
指定中文参数是比较特殊的,这种方式也是jQuery自己扩展的行为,逻辑上也很容易处理
ook.animate({ left: '50', height:'hide' },
height高度在动画结束之后隐藏元素,那么意味着元素本身的高度height也是需要改变的从初始的位置慢慢的递减到0然后隐藏起来
代码中有这么一段,针对hide的动作,我们在done之后会给元素直接隐藏起来
//目标是显示 if (hidden) { jQuery(elem).show(); } else { //目标是隐藏 anim.done(function() { jQuery(elem).hide(); }); }
其实show与hide的流程是一样的,只是针对元素在初始与结束的一个状态的改变
css属性设置独立的缓动函数
在动画预初始化之后(为了支持动画,临时改变元素的一些属性与状态),我们就需要给每一个属性生成一个独立的缓动对象了createTween,主要用于封装这个动画的算法与执行的一些流程操作控制
////////////////// //生成对应的缓动动画 // ////////////////// createTween: function(prop, end) { var tween = jQuery.Tween(elem, animation.opts, prop, end, animation.opts.specialEasing[prop] || animation.opts.easing); //加入到缓动队列 animation.tweens.push(tween); return tween; },
tween对象
通过这个结构大概就知道了,这个就是用于生成动画算法所需要的一些数据与算法的具体流程控制了
属性预处理
- 针对height/width动画的时候,要先处理本身元素溢出
- 针对height/width动画的时候,元素本身的inline状态处理
我们知道元素本身在布局的时候可以用很多属性对其设置,可是一旦进行动画化的话,某些属性的设置可能会对动画的执行产生副作用,所以针对这样的属性,jQuery直接在内部做了最优的处理
如果我们进行元素height/width变化的时候,比如height:1,这样的处理jQuery就需要针对元素做一些强制性的处理
1 添加overflow =“hidden”
2.如果设置了内联并且没有设置浮动 display = "inline-block";
因为内容溢出与内联元素在执行动画的时候,与这个height/width的逻辑是符合的
当然针对这样的修改jQuery非常巧妙了用到了deferred.always方法,我们在执行动画的时候,由于动画的需要改了原始的属性,但是动画在结束之后,我们还是需要还原成其状态
deferred量身定做的always方法,不管成功与失败都会执行这个复原的逻辑
//设置溢出隐藏 if (opts.overflow) { style.overflow = "hidden"; anim.always(function() { //完成后恢复溢出 style.overflow = opts.overflow[0]; style.overflowX = opts.overflow[1]; style.overflowY = opts.overflow[2]; }); }
总结
通过上面不难看出,jQuery动画其实原理上本身是不复杂的。量变产生质变,通过扩展大量的便捷方式加大了逻辑上的难度,但是从根本上来说:
主要包括:
- 属性过滤specialEasing处理的propFilter方法
- 通过Deferred生成流程控制体系
- 通过defaultPrefilter方法对动画执行的临时修正
- 通过createTween方法,生成动画的算法与流程控制器
- 最后通过setInterval来控制每一个createTween对象的执行
大体上jQuery的动画就这么些内容,当然还有一些细节的话 遇到在提出来了,下章就会通过上面的这些处理,实现一个类jquery动画的模拟了,加强理解