浅谈 requestAnimationFrame

3,136 阅读4分钟

背景

在Web应用中,实现动画效果的方法比较多,Javascript 中可以通过定时器 setTimeout或者setInterval 来实现,css3 可以使用 transition 和 animation 来实现,html5 中的 canvas 也可以实现。除此之外,html5 还提供一个专门用于请求动画的API,那就是 requestAnimationFrame,顾名思义就是请求动画帧。

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

实现一个动画

完成一个简单的移动动画,我们可以使用setInterval和requestAnimationFrame两种方式实现。

setInterval

<html>
    <head>
        <title></title>
        <style type="text/css">
            #box {
                margin: 200px;
                width: 200px;
                height: 200px;
                background: green;
            }
        </style>
    </head>

    <body>
        <div id="box"></div>
    </body>
    <script type="text/javascript">
        var element = document.getElementById('box')
        var left = 0;
        var animateCallback = function() {
            element.style.marginLeft = (++left)+ 'px';
            if (left === 500) {
                clearInterval(interval);
            }
        }
        var interval = setInterval(animateCallback, (1000 / 60));
    </script>

使用setInterval我们成功的实现了一个平滑的动画效果,但是我们会发现setInterval的执行时间并不确定,在javascript中,setInterval任务被放进了异步队列中,只有当主线程上的任务执行完以后,才会去检查该队列里的任务是否需要开始执行,所以setInterval的实际执行时机一般要比其设定的时间晚一些。使用setInterval实现动画容易失帧。

什么是丢帧

例如我们使用setInterval进行颜色的切换

var color = ['green', 'red', 'blue', 'yellow'];
var element = document.getElementById('box');
var index = 0;
var animateCallback = function() {
    index++;
    element.style.backgroundColor = color[index];
    if (index === 3){
        clearInterval(interval);
    }
}
var interval = setInterval(interval, 1000 / 100);

上面的动画切换我们设置了间隔10切换一次,但是此时的屏幕刷新频率为16.7,

  • 第0ms时,屏幕未刷新,等待中,setInterval也未执行,等待中;
  • 第10ms时,屏幕未刷新,等待中,setInterval执行颜色切换为green
  • 第16.7ms时: 屏幕刷新,屏幕的box颜色改变为green,setInterval未执行。继续等待
  • 第20ms时:屏幕未刷新,等待中,setInterval执行颜色切换为red,
  • 第30ms时,屏幕未刷新,等待中,setInterval执行颜色切换为blue,
  • 第34.7ms时,屏幕刷新,屏幕的box颜色改变为blue,setTimeout未执行,继续等待中。
  • ...

从上面的执行过程中,我们可以看出,在执行到20ms和30ms直接,setInterval切换了两次颜色,但是屏幕并没有执行一次刷新,就会出现,在34.7ms时,屏幕上直接展示的就是blue颜色,而跳过了red颜色。这就是丢帧现象,这种现象也会引起页面卡顿。

requstAnimationFrame实现

<html>
    <head>
        <title></title>
        <style type="text/css">
            #box {
                margin: 200px;
                width: 200px;
                height: 200px;
                background: green;
            }
        </style>
    </head>

    <body>
        <div id="box"></div>
    </body>
    <script type="text/javascript">
        var start = null;
        var element = document.getElementById('box');
        var left = 0;
        var raf_id = null;
        function animateCallback() {
            element.style.marginLeft = (++left) + 'px';
            if (left === 500) {
                cancelAnimationFrame(raf_id);
            } else {
                raf_id = requestAnimationFrame( animateCallback );
            }
        }
        raf_id =  window.requestAnimationFrame(animateCallback);
    </script>

requestAnimationFrame返回请求的id(整数),我们可以使用这个id来取消请求(cancelAnimationFram(id)),从而停止动画的执行。 和setInterval相比,requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机,requestAnimationFrame的步伐跟着系统的刷新步伐走,它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧。

requestAnimationFrame优势

CPU节能

使用setTimeout实现的动画,当页面被隐藏或最小化时,setTimeout仍然在后台执行动画任务,此时页面处于不可见或者不可用的状态,刷新动画是没有意义的,而且还浪费CPU资源,而rAF则完全不同,当页面处于未激活的状态下,该页面的屏幕绘制任务也会被系统暂停,因此跟着系统步伐走的rAF也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了CPU开销。

函数节流

在高频率事件(resize, scroll)中,为了防止在一个刷新间隔内发生多次函数执行,使用rAF可保证每次绘制间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销,一个绘制间隔内函数执行多次是没有意义的,因为显示器每16.7ms绘制一次,多次绘制并不会在屏幕上体现出来。