通常我们实现 JavaScript 动画,用的都是 setTimeout
或 setInterval
,自己设定一个间隔,利用这两个函数来重复地修改 DOM,产生动画效果。这里面有一个问题,就是间隔究竟设为多少才最合适。太大了动画就不平滑,太小了又会过分消耗计算资源,有时还会丢帧。
理论上说,每一次的 DOM 修改都放在显示器刷新前那一刻进行是最合适的,但是我们利用 JavaScript 还做不到这点,无法准确地知道下一次渲染在什么时刻。一般的显示器刷新频率都设置在 60Hz,刷新间隔便是 1/60 s 约为 17ms。 但也并不是给 setInterval
简单地设置个 17ms 就能解决问题了。requestAnimationFrame
的出现就是为了解决这个的。
语法
window.requestAnimationFrame( callback ) // 也可省略window
requestAnimationFrame()
方法接受一个回调函数 callback
。调用 requestAnimationFrame()
时,浏览器就会在下一次渲染之前执行 callback
,然后渲染。所以对 DOM 的修改就可以放在 callback
里面。另外像 setTimeout
一样,我们要在回调函数中再次调用 requestAnimationFrame()
,才能将每一帧动画衔接起来。requestAnimationFrame()
的返回值是一个唯一的 id。
跟 setTimeout
、setInterval
的区别就是,requestAnimationFrame
不能自行设定间隔,一旦调用了,浏览器就会自动在下次渲染前的一刻执行代码,所以这个执行的时机一般是在 17ms 以内。 因为对 DOM 的修改和浏览器渲染的步调做到了一致,所以效率更高,动画会很平滑。用 requestAnimationFrame
的另外一个好处就是,当标签页不活动的时候,动作就会暂停,减少了不必要的内存消耗。
浏览器差异
此方法目前还是草案状态,各浏览器的实现不太一致,IE 从版本 10 开始提供支持,其他浏览器的旧版本需要加对应前缀。Chrome 的方法还支持第二个参数:webkitRequestAnimationFrame( callback, element )
,element
表示属性发生改变的 DOM 元素,这样渲染会局限在该元素中,提高效率。Firefox 的实现 mozRequestAnimationFrame
会在执行回调时,给 callback
传入一个参数 timestamp
,是以毫秒表示的时间,表示下一次渲染的精确时刻。
要取消一个 requestAnimationFrame()
,可以用 cancelAnimationFrame
,并传入 id。Chrome 旧版本中还使用过两个名称:webkitCancelAnimationFrame()
和 webkitCancelRequestAnimationFrame()
。
兼容写法
Paul Irish 等人提供了一个较好的写法,兼容了各浏览器,同时在不支持 requestAnimationFrame()
的浏览器中回退到 setTimeout
,也就是 IE6+ 都无压力:
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame) window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() {
callback(currTime + timeToCall);
}, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}());