requestAnimationFrame制作动画

2,493 阅读3分钟

requestAnimationFrame的概述

requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧,即是时间间隔为1000/60=16.7ms。 requestAnimationFrame 与 setTimeout相比,最大的优势在于requestAnimationFrame是由系统来决定回调函数的执行时机,充分利用显示器的刷新机制,比较节省系统资源。当页面处于不可见或不可用状态时,浏览器就会停止动画,这意味着更少的CPU和更少的内存消耗。

浏览器丢帧的原因

在使用requestAnimationFrame之前,若是我们使用js制作动画效果的话,一般都是通过setTimeoutsetInterval来实现的,但是,你在setTimeout或setInterval中指定的回调函数的执行时机是无法保证的,它将在这一帧动画的某个时间点被执行,很可能是在帧结束的时候,这就意味这我们可能失去这一帧的信息。

举个例子,当动画使用10ms的JavaScript计时器分辨率绘制动画时,你将看到一个时序不匹配,如下所示。

最上面的一行代表大多数监视器上显示的16.7ms显示频率,最下面的一行代表10ms的典型setTimeout。由于在显示刷新间隔之前发生了另一个绘制请求,因此无法绘制每三个绘制(用红色箭头指示)。这种透支会导致动画断断续续,因为每三帧都会丢失。计时器分辨率的降低也会对电池寿命产生负面影响,并降低其他应用程序的性能。
requestAnimationFrame方法(在万维网联盟(W3C)的定义为基于脚本的动画的定时控制规范)可以解决丢帧问题,因为它使应用程序时通知(且仅当)的浏览器需要更新页面显示。因此,应用程序与浏览器绘画间隔完美匹配,并且仅使用适当数量的资源。从setTimeout切换到 requestAnimationFrame很容易,因为它们都安排了一个回调。对于连续动画,在调用动画函数之后再次调用requestAnimationFrame。

使用requestAnimationFrame

requestAnimationFrame使用一个回调函数作为参数。这个回调函数会在浏览器重绘之前调用。

requestID = window.requestAnimationFrame(callback); 

使用requestAnimationFrame的时候,只需反复调用它即可。

function repeatOften() {
  // Do whatever
  requestAnimationFrame(repeatOften);
}

requestAnimationFrame(repeatOften)

使用cancelAnimationFrame方法取消重绘,它的参数是requestAnimationFrame返回的一个代表任务ID的整数值。

window.cancelAnimationFrame(requestID);

requestAnimationFrame的兼容性

由于requestAnimationFrame目前还存在兼容性问题,而且不同的浏览器还需要带不同的前缀,如果不支持requestAnimationFrame和cancelAnimationFrame,则使用setTimeout和clearTimeout。兼容性封装:

  • 简化版
window.requestAnimationFrame = window.requestAnimationFrame ||
  window.webkitRequestAnimationFrame ||
  window.mozRequestAnimationFrame ||
  window.msRequestAnimationFrame ||
  function (callback) {
    //为了使setTimteout的尽可能的接近每秒60帧的效果
    window.setTimeout(callback, 1000 / 60)
  }

window.cancelAnimationFrame = window.cancelAnimationFrame ||
  Window.webkitCancelAnimationFrame ||
  window.mozCancelAnimationFrame ||
  window.msCancelAnimationFrame ||
  function (id) {
    window.clearTimeout(id)
  };
  • 升级版
(function() {
    var lastTime = 0;
    var vendors = ['webkit', 'moz', 'ms'];
    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);
        };
   	}
}());

好了,我们大概了解了requestAnimationFrame函数,那么我们接下来就学以致用,制作一个进度条效果吧

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用requestAnimationFrade制作进度条</title>
    <style>
        .container {
            width: 400px;
            margin: 50px auto;
        }
        .wrap {
            height: 10px;
            border-radius: 5px;
            overflow: hidden;
            background-color: #31493C;
        }
        .box {
            height: 6px;
            background-color: #B3EFB2;
            border-radius: 3px;
            margin: 2px;
        }
        .row {
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .btn {
            cursor: pointer;
            margin: 0 10px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="wrap">
            <div class="box" id="box"></div>
        </div>
        <div class="row">
            <div class="txt"><span id="txt">0</span>%</div>
            <div class="btn" id="btn">暂停</div>
        </div>
    </div>
    <script>
        let box = document.getElementById('box');
        let btn = document.getElementById('btn');
        let handel = 0;
        let $width = 0;
        function setWidth (params) {
            box.style.width = $width + 'px';
            handel = window.requestAnimationFrame(setWidth);
            $width <= 396 ? $width++ : btn.style.display = 'none';
            document.getElementById('txt').innerText = Math.ceil($width/400*100)
        }
        setWidth();
        btn.addEventListener('click', function () {
            if (handel) {
                window.cancelAnimationFrame(handel);
                handel = 0;
                btn.innerText = '播放'
            } else {
                setWidth();
                btn.innerText = '暂停'
            }
        }, false)
    </script>
</body>
</html>

参考资料