在网页中,实现动画无外乎两种方式。
1. CSS3 方式,也就是利用浏览器对CSS3 的原生支持实现动画;
2. 脚本方式,经过间隔一段时间用JavaScript 来修改页面元素样式来实现动画。
接下来咱们就分别介绍这两种方式的原理,让你们先对这两种方式有一个直观认识,了解各自的优缺点。javascript
CSS3 的方式下,开发者通常在css 中定义一些包含CSS3 transition 语法的规则。在某些特定状况下,让这些规则发生做用,因而浏览器就会将这些规则应用于指定的DOM元素上,产生动画的效果。这种方式毫无疑问运行效率要比脚本方式高,由于浏览器原生支持,省去了JavaScript 的解释执行负担,有的浏览器(好比Chrome 浏览器)甚至还能够充分利用GPU 加速的优点,进一步加强了动画渲染的性能。不过CSS3 的方式并不是完美,也有很多缺点。
首先, CSS3 Transition 对一个动画规则的定义是基于时间和速度曲线( Speed Curve)的规则。换句话来讲,就是CSS3 的动画过程要描述成“在什么时间范围内,以什么样的运动节奏完成动画” 。css
<!DOCTYPE html> <html> <head> <style> .sample { background: red; position: absolute; left: 0px; width: 100px; height: 100px; transition-property: left; transition-duration: 0.5s; transition-timing-function: ease } .sample:hover { left: 420px; } </style> </head> <body> <div class="sample" /> </body> </html>
在上面的例子中, sample 类的元素定义了这样的动画属性:“ left 属性会在0.2 秒内以ease 速度曲线完成动画” 。transition 只定义了动画涉及的属性、时间和速度曲线,并不定义须要修改的具体值。sample 类的left 属性默认值为0 ,当鼠标移到sample 类元素上时, left 属性就拥有新的值420px 。这时候transition 定义的规则发生做用,让left 属性以ease 速度曲线在0.2 秒
的时间完成从0 变成420px 的转化过程,这个过程当中,用户看到的就是sample 类元素向右移动420 个像素的动画过程。
由于CSS3 定义动画的方式是基于时间和速度曲线,可能不利于动画的流畅,由于动画是可能会被中途打断的,在上面的例子中,鼠标移到sample 类元素上的时候开始动画,可是在0.2 秒的动画时间内,用户的鼠标可能会移出这个sample 类元素,这时候CSS3 还会以ease 速度曲线的节奏让sample 类元素回到原位。从用户体验角度来讲,中途sample 类元素回到原位的动做,语义上是“取消操做”的含义,但却依然以一样的时间和ease 节奏来完成“取消操做”的动画,这并不合理。html
时间和速度曲线的不合理是CSS3 先天的属性,更让开发者头疼的就是开发CSS3 规则的过程,尤为是对transition-duration 时间很短的动画调试,由于CSS3 的transition 过程老是一闪而过,捕捉不到中间状态,只能一遍一遍用肉眼去检验动画效果,用CSS3作过复杂动画的开发者确定都深有体会。虽然CSS3 有这样一些缺点,可是由于其无与伦比的性能,用来处理一些简单的动画仍是不错的选择。java
相对于CSS3 方式,脚本方式最大的好处就是更强的灵活度,开发者能够任意控制动画的时间长度,也能够控制每一个时间点上元素渲染出来的样式,能够更容易作出丰富的动画效果。脚本方式的缺点也很明显,动画过程经过JavaScript 实现,不是浏览器原生支持,消耗的计算资源更多。若是处理不当,动画可能会出现卡顿滞后现象,原本使用动画是为了创造更好的用户体验,若是出现卡顿,反而对用户体验带来很差的影响。最原始的脚本方式就是利用setlnterval 或者setTimeout 来实现,每隔一段时间一个指定的函数被执行来修改界面的内容或者样式,从而达到动画的效果。浏览器
<!DOCTYPE html> <html> <head> <style> #sample { position: absolute; background: red; width: 100px; height: 100px; } </style> </head> <body> <div id="sample" /> <script type="text/javascript"> var animatedElement = document.getElementById("sample"); var left = 0; var timer; var ANIMATION_INTERVAL = 16; timer = setInterval(function() { left += 10; animatedElement.style.left = left + "px"; if ( left >= 400 ) { clearInterval(timer); } }, ANIMATION_INTERVAL); </script> </body> </html>
在上面的例子中,有一个常量ANIMATION INTERVAL 定义为16 , setlnterval 以这个常量为间隔,每16 毫秒计算一次sample 元素的left 值,每次都根据时间推移按比例增长left 的值,直到left 大于400 。为何要选择16 毫秒呢?由于每秒渲染60 帧(也叫60fps, 60 Frame Per Second)会给用户带来足够流畅的视觉体验,一秒钟有1000 毫秒, 1000 /60 =16 ,也就是说,若是咱们作到每16 毫秒去渲染一次画面,就可以达到比较流畅的动画效果。对于简单的动画, setlnterval 方式勉强可以及格,可是对于稍微复杂一些的动画,脚本方式就顶不住了,好比渲染一帧要花去超过32 毫秒的时间,那么还用16 毫秒一个间隔的方式确定不行。实际上,由于一帧渲染要占用网页线程32 毫秒,会致使setlnterval根本没法以16 毫秒间隔调用渲染函数,这就产生了明显的动画滞后感,本来一秒钟完成的动画如今要花两秒钟完成,因此这种原始的setlnterval 方式是确定不适合复杂的动画的。
出现上面问题的本质缘由是setlnterval 和setTimeout 并不能保证在指定时间间隔或者延迟的状况下准时调用指定函数。因此能够换一个思路,当指定函数调用的时候,根据逝去的时间计算当前这一帧应该显示成什么样子,这样即便由于浏览器渲染主线程忙碌致使一帧渲染时间超过16 毫秒,在后续帧谊染时至少内容不会所以滞后,即便达不倒60fps 的效果,也能保证动画在指定时间内完成。函数
<!DOCTYPE html> <html> <head> <style> #sample { position: absolute; background: red; width: 100px; height: 100px; } </style> </head> <body> <div id="sample" /> <script type="text/javascript"> var lastTimeStamp = new Date().getTime(); function raf(fn) { var currTimeStamp = new Date().getTime(); var delay = Math.max(0, 16 - (currTimeStamp - lastTimeStamp)); var handle = setTimeout(function(){ fn(currTimeStamp); }, delay); lastTimeStamp = currTimeStamp; return handle; } var left = 0; var animatedElement = document.getElementById("sample"); var startTimestamp = new Date().getTime(); function render(timestamp) { left += (timestamp - startTimestamp) / 16; animatedElement.style.left = left + 'px'; if (left < 400) { raf(render); } } raf(render); </script> </body> </html>
在上面定义的raf 中,接受的fn 函数参数是真正的渲染过程, raf 只是协调渲染的节奏。raf 尽可能以每隔16 毫秒的速度去调用传递的fn 参数,若是发现上一次被调用时间和这一次被调用时间相差不足16 毫秒,就会保持16 毫秒一次的渲染间隔继续,若是发现
两次调用时间间隔已经超出了16 毫秒,就会在下一次时钟周期马上调用fn 。上面的render 函数中根据当前时间和开始动画的时间差来计算sample 元素的left 属性,这样不管render 函数什么时候被调用,总可以渲染出正确的结果。
最后,咱们将render 做为参数传递给raf ,启动了动画过程性能