深刻理解HTML5定时器requestAnimationFrame

前言

计时器一直是javascript动画的核心技术。而编写动画循环的关键是要知道延迟时间多长合适。一方面,循环间隔必须足够短,这样才能让不一样的动画效果显得平滑流畅;另外一方面,循环间隔还要足够长,这样才能确保浏览器有能力渲染产生的变化。javascript

大多数电脑显示器的刷新频率是60Hz,大概至关于每秒钟重绘60次。大多数浏览器都会对重绘操做加以限制,不超过显示器的重绘频率,由于即便超过那个频率用户体验也不会有提高。所以,最平滑动画的最佳循环间隔是1000ms/60,约等于16.6ms。html

而setTimeout和setInterval的问题是,它们都不精确。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器UI线程队列中以等待执行的时间。若是队列前面已经加入了其余任务,那动画代码就要等前面的任务完成后再执行。java

requestAnimationFrame采用系统时间间隔,保持最佳绘制效率,不会由于间隔时间太短,形成过分绘制,增长开销;也不会由于间隔时间太长,使用动画卡顿不流畅,让各类网页动画效果可以有一个统一的刷新机制,从而节省系统资源,提升系统性能,改善视觉效果。web

特色

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

用法

requestAnimationFrame方法将一个callback做为参数,回调函数会被传入一个参数,DOMHighResTimeStamp,指示当前被 requestAnimationFrame() 排序的回调函数被触发的时间。返回值是一个请求 ID,表示回调列表中惟一的标识。能够传这个值给 window.cancelAnimationFrame() 以取消回调函数。面试

requestID = window.requestAnimationFrame(callback);

利用这个api,能够将某些代码放到下一次从新渲染时执行,避免短期内触发大量reflow。
例如页面滚动事件(scroll)的回调函数就很适合使用这个api, 将回调操做推迟到下一次从新渲染。但须要注意的是requestAnimationFrame 无论理回调函数,即在回调被执行前,屡次调用带有同一回调函数的 requestAnimationFrame,会致使回调在同一帧中执行屡次。最简单的,能够用节流函数来解决这个问题,也能够想办法让requestAnimationFrame的队列里一样的回调函数只有一个:api

let scheduledAnimationFrame = false;
document.body.onscroll = () => {
    if (scheduledAnimationFrame) return;
    scheduledAnimationFrame = true;
    window.requestAnimationFrame(() => {
        scheduledAnimationFrame = false;
        // do something
    });
};

固然,最佳的应用场景仍是在帧动画里,能够大幅优化性能。浏览器

面试题

如何渲染几万条数据并不卡住界面app

这道题考察了如何在不卡住页面的状况下渲染数据,也就是说不能一次性将几万条都渲染出来,而应该一次渲染部分 DOM,那么就能够经过 requestAnimationFrame 来每 16 ms 刷新一次。dom

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <ul>控件</ul>
  <script>
    setTimeout(() => {
      // 插入十万条数据
      const total = 100000
      // 一次插入 20 条,若是以为性能很差就减小
      const once = 20
      // 渲染数据总共须要几回
      const loopCount = total / once
      let countOfRender = 0
      let ul = document.querySelector("ul");
      function add() {
        // 优化性能,插入不会形成回流
        const fragment = document.createDocumentFragment();
        for (let i = 0; i < once; i++) {
          const li = document.createElement("li");
          li.innerText = Math.floor(Math.random() * total);
          fragment.appendChild(li);
        }
        ul.appendChild(fragment);
        countOfRender += 1;
        loop();
      }
      function loop() {
        if (countOfRender < loopCount) {
          window.requestAnimationFrame(add);
        }
      }
      loop();
    }, 0);
  </script>
</body>
</html>

兼容性

一些老的浏览器是不支持这个api的,为了也能使用这个api, 能够自定义这个方法,挂载到window下面:函数

(function() {
    var lastTime = 0;
    var vendors = ['webkit', 'moz'];
    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) {
            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);
        };
}());
相关文章
相关标签/搜索