前言
很长一段时间,JavaScript的动画效果都是通过setTimeOut和setInterval来实现的,虽然使用CSS transition 和 animation进行Web动画开发实现更为方便,但多年来以JavaScript为基础来实现动画却很少有所改善。直到FireFox 4的发布,才带来了第一种对JavaScript动画的改善方法。
使用setTimeout和setInterval实现动画存在几个关键的问题:
- 时间间隔应该设置为多少才是合适的?
- 设置的时间间隔是否真的会按预想执行?
- 回调函数如何进行编写才能高效执行?
- 与平台和浏览器是否相关?
但事实上由于浏览器的事件循环机制、渲染机制以及其他原因的存在,使用setTimeout 和 setInterval解决上述问题是非常困难的。
setTimeout 和 setInterval
以Webkit ( 本文主要指chrome浏览器 )为例, setTimeout 和 setInterval 的实现机制是类似的,区别主要在于后者是重复性的。
Webkit 会为DOM中每一个setTimeout 和 setInterval 的调用创建DOMTimer, 随后该对象会由被存储于TLS( Thread localstorage )中的 ThreadTimers 进行管理。 ThreadTimers 内部其实是一个最小堆, 每次取出timeout时间最小的进行执行,同时会合并时间相同的Timer。
Timer超时后,浏览器会清除该Timer对象,同时调用其回调函数,如果回调函数中涉及修改页面的样式或布局,则会触发重新layout计算,从而触发立即重新绘制一个新帧。
由上述实现机制的描述可以想到以下两点:
- setTimeout 和 setInterval 会强制要求浏览器在某个时间后执行其回调函数,无论此时主线程是否繁忙或者页面是否被隐藏 (部分浏览器可能做过相关优化, 如Chrome)
- setTimeout 和 setInterval 间隔时间的设置是随意的,这可能会造成资源的浪费。例如,浏览器的刷新率只有60hz,然而设置的时间间隔是5ms,事实上用户根本无法感知到这些变化,但却消耗了大量的资源。
requestAnimationFrame
现在关注到requestAnimationFrame ,其实现原理如下:
- 注册回调函数
- 浏览器更新时触发animate
- animate 触发所有注册过的callback
具体的工作机制可以理解为控制权转移,把帧更新何时更新的控制权交给浏览器,浏览器在下一帧绘制前调用其设置的回调函数,完成JavaScript对动画所做的设置和逻辑。这样做既可以避免浏览器更新与动画帧更新的不同步,又可以给予浏览器足够大的优化空间。
其他注意的点
- 浏览器会将 setTimeout 或 setInterval 的五层或更多层嵌套调用(调用五次之后)的最小延时限制在 4ms。这是历史遗留问题。
- 嵌套的 setInterval 并不能保证两次执行的延时。
对 setInterval 而言,内部的调度程序会每间隔 100 毫秒执行一次 func(i++):
使用 setInterval 时,func 函数的实际调用间隔要比代码中设定的时间间隔要短。因为 func 的执行所花费的时间“消耗”了一部分间隔时间。极端情况下, 如果func的每次执行时间都超过 delay 设置的时间,那么每次调用之间将完全没有停顿。
- 需要注意到所有的调度方法都不能确保确切的延时执行。例如浏览器中的计时器可能会因为诸多原因变慢:
- CPU过载
- 浏览器页签处于后台模式。
- 笔记本电脑使用电池供电
而所有这些因素,可能会将定时器的最小计时器分辨率(最小延迟)增加到 300ms 甚至 1000ms。
- 对于requestAnimationFrame,当页面不可见时,其回调函数不会被调用。
