requestAnimationFrame

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2022-06-19

转载:

requestAnimationFrame 详解 requestAnimationFrame 知多少?

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

屏幕绘制频率

市面上常见的显示器有两种,即 CRT 和 LCD, CRT 是一种使用阴极射线管(Cathode Ray Tube)的显示器,LCD 就是我们常说的液晶显示器( Liquid Crystal Display)。

CRT 是一种使用阴极射线管的显示器,屏幕上的图形图像是由一个个因电子束击打而发光的荧光点组成,由于显像管内荧光粉受到电子束击打后发光的时间很短,所以电子束必须不断击打荧光粉使其持续发光。电子束每秒击打荧光粉的次数就是屏幕绘制频率。

而对于 LCD 来说,则不存在绘制频率的问题,因为 LCD 中每个像素都在持续不断地发光,直到不发光的电压改变并被送到控制器中,所以 LCD 不会有电子束击打荧光粉而引起的闪烁现象。

因此,当你对着电脑屏幕什么也不做的情况下,显示器也会以每秒 60 次的频率正在不断的更新屏幕上的图像。为什么你感觉不到这个变化? 那是因为人的眼睛有视觉停留效应,即前一副画面留在大脑的印象还没消失,紧接着后一副画面就跟上来了,这中间只间隔了 16.7ms(1000/60≈16.7), 所以会让你误以为屏幕上的图像是静止不动的,类似于翻页动画的原理。如果每秒 1 次(每秒只翻一页)的频率刷新将会感受到严重的闪烁。

setTimeout 和 setTimeInterval

setTimeout 和 setTimeInterval 其实就是通过设置一个间隔时间来不断的改变图像的位置,从而达到动画效果的,但是

// 实现一个 无限循环的动画
var e = document.getElementById("e");
var flag = true;
var left = 0;

function render() {
  if (flag) {
    if (left >= 100) {
      flag = false;
    }
    e.style.left = ` ${left++}px`;
  } else {
    if (left <= 0) {
      flag = true;
    }
    e.style.left = ` ${left--}px`;
  }
}
setInterval(function () {
  render();
}, 1000 / 60);

这两个 API 的不足之处:

  • setTimeout 的执行时间并不是确定的。在 JavaScript 中, setTimeout 任务被放进了异步队列中,只有当主线程上的任务执行完以后,才会去检查该队列里的任务是否需要开始执行,所以 setTimeout 的实际执行时机一般要比其设定的时间晚一些。

  • 刷新频率受 屏幕分辨率 和 屏幕尺寸 的影响,不同设备的屏幕绘制频率可能会不同,而 setTimeout 只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相同。

以上两种情况都会导致 setTimeout 的执行步调和屏幕的刷新步调不一致,从而引起丢帧现象。 那为什么步调不一致就会引起丢帧呢?

首先要明白,setTimeout 的执行只是在内存中对元素属性进行改变,这个变化必须要等到屏幕下次绘制时才会被更新到屏幕上。如果两者的步调不一致,就可能会导致中间某一帧的操作被跨越过去,而直接更新下一帧的元素。假设屏幕每隔 16.7ms 刷新一次,而 setTimeout 每隔 10ms 设置图像向右移动 1px,就会出现如下绘制过程:

  • 第 0 ms:屏幕未绘制,等待中,setTimeout 也未执行,等待中;
  • 第 10 ms:屏幕未绘制,等待中,setTimeout 开始执行并设置元素属性 left=1px;
  • 第 16.7 ms:屏幕开始绘制,屏幕上的元素向左移动了 1px, setTimeout 未执行,继续等待中;
  • 第 20 ms:屏幕未绘制,等待中,setTimeout 开始执行并设置 left=2px;
  • 第 30 ms:屏幕未绘制,等待中,setTimeout 开始执行并设置 left=3px;
  • 第 33.4 ms:屏幕开始绘制,屏幕上的元素向左移动了 3px, setTimeout 未执行,继续等待中;

从上面的绘制过程中可以看出,屏幕没有更新 left=2px 的那一帧画面,元素直接从 left=1px 的位置跳到了 left=3px 的的位置,这就是丢帧现象,这种现象就会引起动画卡顿。

rAF

与 setTimeout 相比,rAF 最大的优势是 由系统来决定回调函数的执行时机。具体一点讲就是,系统每次绘制之前会主动调用 rAF 中的回调函数,如果系统绘制率是 60Hz,那么回调函数就每 16.7ms 被执行一次,如果绘制频率是 75Hz,那么这个间隔时间就变成了 1000/75=13.3ms。换句话说就是,rAF 的执行步伐跟着系统的绘制频率走。它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。

另外两个优势,

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

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

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

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #e {
      width: 200px;
      height: 200px;
      position: absolute;
      background-color: pink;
    }
  </style>
</head>

<body>
  <div id="e">

  </div>
  <script>
    var e = document.getElementById("e");
    var flag = true;
    var left = 0;
    var start = null;
    // 默认情况下,requestAnimationFrame执行频率是1000/60,大概是16ms多执一次。
    // 如果我们想每40ms执行一次怎么办呢?
    var diffTime = 40;

    function render() {
      if (flag == true) { // 往右移
        if (left >= 100) {
          flag = false;
        }
        e.style.left = ` ${left++}px`;
      } else { // 往左移
        if (left <= 0) {
          flag = true; // 到边界后改变方向
        }
        e.style.left = ` ${left--}px`;
      }
    }

    (function animationLoop(timestamp) {
      // 传递给requestAnimationFrame的回调函数animationLoop会在浏览器下一次重绘之前执行
      // timestamp是requestAnimationFrame传给回调的一个参数
      // 它表示requestAnimationFrame() 开始去执行回调函数的时刻。
      // 这个时刻不是时间戳,而是记录 如果第一次执行是10ms 如果是保持每16.66ms执行一次,下一次这个时刻就是26.66 

      // 记录render执行的时刻
      if (!start) {
        start = timestamp; // 当做第一次与回调一起执行
      }

      console.log(timestamp - start);
      // 打印的数值是16.66 33.33 49.99 不断循环,因为设置的 diff是 40,超过40之后,就会执行一次 render,start就会被重置

      // 当前执行回调的时刻 - 上次render执行的时刻 大于diffTime,那么执行动画,并更新上次执行时间
      if (timestamp - start > diffTime) {
        render();
        start = timestamp;
      }
      requestAnimationFrame(animationLoop);
    })();
  </script>
</body>

</html>