requestAnimationFrame

229 阅读4分钟

背景

在 Web 应用中,实现动画效果方法很多。JavaScript 可以通过设置定时器(setTimeout 和 setInterval)来实现,CSS3 可以通过 transition 过渡效果和 animation 动画实现,HTML5 中的 canvas 也可以实现。除此之外,HTML5 还提供一个专门用于请求动画的API,那就是 requestAnimationFrame(请求动画帧)。

传统的 javascript 动画通过定时器 setTimeout 或者 setInterval 实现。但是定时器动画一直存在两个问题,第一个就是动画的循环时间间隔不好确定,设置长了动画显得不够平滑流畅,设置短了浏览器的重绘频率会达到瓶颈,推荐的最佳循环间隔是17ms(大多数电脑的显示器刷新频率是60Hz,1000ms/60);第二个问题是定时器第二个时间参数只是指定了多久后将动画任务添加到浏览器的UI线程队列中,如果UI线程处于忙碌状态,那么动画不会立刻执行。为了解决这些问题,H5 中加入了 requestAnimationFrame

与 setTimeout() 和 setInterval() 方法不同, requestAnimationFrame() 不需要调用者指定帧速率,浏览器会自行决定最佳的帧效率。

setTimeout 和 setInterval 的不足

  1. setTimeout 和 setInterval 从不考虑浏览器内部发生了其他什么事,它只要求浏览器在某个时间之后调用它的回调函数,无论浏览器很繁忙或者页面被隐藏(虽然某些浏览器做了这方面的优化,例如 chromium);
  2. setTimeout 和 setInterval 只要求浏览器做什么,而不管浏览器能不能做到(例如 mainloop 有很多事件需要处理),这有点强人所难,而且会带来极大的资源浪费。举个例子,例如屏幕的刷新率是 60HZ,但是设置的时间间隔是 5ms,其实对用户来说根本看不到这些变化,但是额外需要消耗更多的 CPU 资源,太不环保了…
  3. setTimeout 和 setInterval 可能是编程风格方面的考虑。如果每一帧可能在不同的代码出需要设置回调函数,一个方法是统一到一个地方,但是这有点勉为其难,另一个方法是分别用 setInterval 设置它们,这个方法的问题是,浏览器可能需要计算更多次,刷新更多次的屏幕。

requestAnimationFrame 是什么

window.requestAnimationFrame()  告诉浏览器——你希望执行一个动画。代码中使用这个API,就是让浏览器在下一个动画帧安排一次网页重绘。并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

注意:若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用 window.requestAnimationFrame()

语法

window.requestAnimationFrame(callback);

callback 表示下次重绘之前执行的回调函数,并返回一个 ID,表示一个唯一的标识,可以通过 window.cancelAnimationFrame(ID) 取消回调函数。

优点

requestAnimationFrame 是浏览器用于定时循环操作的一个接口,类似于 setTimeout,主要用途是按帧对网页进行重绘。

requestAnimationFrame 的优势,在于充分利用显示器的刷新机制,比较节省系统资源。显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame 的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。

  1. requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。
  2. 在隐藏或不可见的元素中,requestAnimationFrame 将不会进行重绘或回流,这当然就意味着更少的 CPU、GPU 和内存使用量。
  3. requestAnimationFrame 是由浏览器专门为动画提供的 API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了 CPU 开销。

缺点

有一点需要注意,requestAnimationFrame 是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame 的动画效果会大打折扣。

示例

var deg = 0;
var id;
var div = document.getElementById("div");
div.addEventListener('click', function () {
    var self = this;
    requestAnimationFrame(function change() {
        self.style.transform = 'rotate(' + (deg++) + 'deg)';
        // 在这里再次调用 requestAnimationFrame 使得下次重绘之前再次执行这个回调函数
        id = requestAnimationFrame(change);
    });
});
document.getElementById('stop').onclick = function () {
    cancelAnimationFrame(id);
};