Web动画优化之requestAnimationFrame深入理解

前言

在开发Web应用时,实现动画效果是很常见的开发需求,实现动画的方式也是多种多样。本文将从动画原理出发,对比实现动画的常见方式,深入理解使用requestAnimationFrame实现动画的优势。

动画原理

为了深入理解 requestAnimationFrame 背后的原理,我们首先需要了解一下屏幕刷新频率。屏幕刷新频率即图像在屏幕上更新的速度,也即屏幕上的图像每秒钟出现的次数,它的单位是赫兹(Hz)。大多数电脑显示器的刷新频率是60Hz,大概相当于每秒钟重绘60次。

根据上面的原理我们知道,你所看到的屏幕上的图像正在以每秒60次的频率刷新,为什么你感觉不到这个刷新? 那是因为人的眼睛有视觉停留效应,即前一副画面留在大脑的印象还没消失,紧接着后一副画面就跟上来了,这中间只间隔了16.7ms(1000/60≈16.7), 所以会让你误以为屏幕上的图像并没有在刷新。而动画本质就是要让人眼看到图像被刷新而引起变化的视觉效果,这个变化要以连贯的、平滑的方式进行过渡。 那怎么样才能做到这种效果呢?

刷新频率为60Hz的屏幕每16.7ms刷新一次,我们在屏幕每次刷新前,将图像的位置向左移动一个像素,即1px。这样一来,屏幕每次刷出来的图像位置都比前一个要差1px,因此你会看到图像在移动;由于我们人眼的视觉停留效应,当前位置的图像停留在大脑的印象还没消失,紧接着图像又被移到了下一个位置,因此你才会看到图像在流畅的移动,这就是视觉效果上形成的动画。

通过setTimeout实现动画效果

理解了上面的概念以后,我们会想到通过setTimeout设置一个间隔时间来不断的改变图像的位置,从而达到动画效果的。但我们会发现,利用seTimeout实现的动画在某些低端机上会出现卡顿、抖动的现象。这种现象的产生主要有以下两个原因:

  1. setTimeout的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器UI线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。因此 setTimeout的实际执行时间一般要比其设定的时间晚一些。
  2. 不同设备的屏幕刷新频率可能会不同,而 setTimeout只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相同。如果时间间隔太短,就会造成过度绘制,增加开销;如果太长,则会使动画卡顿不流畅。

通过requestAnimationFrame实现动画效果

与setTimeout不同,requestAnimationFrame不需要设置时间间隔,是由系统来决定回调函数的执行时机。具体一点讲,如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。

requestAnimationFrame的用法与setTimeoutt很相似,只是不需要设置时间间隔而已。requestAnimationFrame使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。它返回一个整数,表示定时器的编号,这个值可以传递给cancelAnimationFrame用于取消这个函数的执行。

var timer = requestAnimationFrame(function () {
// 动画逻辑
});
cancelAnimationFrame(timer);

兼容方案

由于requestAnimationFrame目前还存在兼容性问题,而且不同的浏览器还需要带不同的前缀。因此需要通过优雅降级的方式对requestAnimationFrame进行封装,优先使用高级特性,然后再根据不同浏览器的情况进行回退,直到只能使用setTimeout的情况。

if (!window.requestAnimationFrame) {
    var lastTime = 0;
    window.requestAnimationFrame = function (callback) {
        var currTime = new Date().getTime();
        var timeToCall = Math.max(0, 16.7 - (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);
    };
}

也可使用github上提供的requestAnimationFrame polyfill兼容库,地址https://github.com/darius/requestAnimationFrame

总结

计时器一直是JavaScript动画的核心技术,而编写动画循环的关键是要知道延迟时间多长合适。requestAnimationFrame采用系统时间间隔,保持最佳绘制效率,节省系统资源,提高系统性能。具体优势有以下几点:

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注