requestAnimationFrame 到底解决的是什么问题?

2,639 阅读5分钟

这是我参与8月更文挑战的第19天,活动详情查看8月更文挑战

前言

上次我们讲解了 setTimeout 和 setInterval,对其有了一些理解,requestAnimationFrame 又是什么?为什么会有 requestAnimationFrame 呢?今天我们就来分析下,开始吧。

html5 提供一个专门用于请求动画的 API,即 requestAnimationFrame,顾名思义就是 “请求动画帧”。 为了深入理解 requestAnimationFrame 背后的原理,我们首先需要了解一下与之相关的几个概念:

屏幕绘制频率

即图像在屏幕上更新的速度,也即屏幕上的图像每秒钟出现的次数,它的单位是赫兹(Hz)。 对于一般笔记本电脑,这个频率大概是60Hz, 可以在桌面上 右键 > 屏幕分辨率 > 高级设置 > 监视器 中查看和设置。这个值的设定受屏幕分辨率、屏幕尺寸和显卡的影响,原则上设置成让眼睛看着舒适的值都行。

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

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

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

因此,当你对着电脑屏幕什么也不做的情况下,显示器也会以每秒60次的频率正在不断的更新屏幕上的图像。为什么你感觉不到这个变化? 那是因为人的眼睛有视觉停留效应,即前一副画面留在大脑的印象还没消失,紧接着后一副画面就跟上来了,这中间只间隔了16.7ms(1000/60≈16.7), 所以会让你误以为屏幕上的图像是静止不动的。而屏幕给你的这种感觉是对的,试想一下,如果刷新频率变成1次/秒,屏幕上的图像就会出现严重的闪烁,这样就很容易引起眼睛疲劳、酸痛和头晕目眩等症状。

CSS 动画原理

根据上面的原理我们知道,你眼前所看到图像正在以每秒 60 次的频率绘制,由于频率很高,所以你感觉不到它在绘制。而 动画本质就是要让人眼看到图像被绘制而引起变化的视觉效果,这个变化要以连贯的、平滑的方式进行过渡。 那怎么样才能做到这种效果呢? 

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

requestAnimationFrame 原理

因为 setTimeout 和 setInterval 是异步 api,必须需要等同步任务执行,还需要等待微任务完成以后,然后才会去执行当前这个回调函数。

这里会存在一个问题,没有办法去精准地把时间定位到,哪怕你写成 16,它也没有办法,让时间精准定位到 16。时间间隔没有办法保证。

与 setTimeout 相比,requestAnimationFrame 最大的优势是 由系统来决定回调函数的执行时机。

如果系统绘制率是 60Hz,那么回调函数就每16.7ms 被执行一次,如果绘制频率是75Hz,那么这个间隔时间就变成了 1000/75=13.3ms,也就是说它的时间间隔,是跟着系统的绘制频率走。

它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。

requestAnimationFrame 用法

用法其实跟 setTimeout 完全一致,只不过当前的时间间隔是跟着系统的绘制频率走,是固定的。

取消回调函数的话,使用 cancelAnimationFrame(timer1)

// 调用的是系统的时间间隔
var timer1 = requestAnimationFrame(function() {
  console.log(1);
});
var timer2 = requestAnimationFrame(function() {
  console.log(2);
});
var timer3 = requestAnimationFrame(function() {
  console.log(3);
});

cancelAnimationFrame(timer1);

requestAnimationFrame 兼容性处理

这里的兼容性处理,主要就是当不支持 requestAnimationFrame 的时候,使用 setTimeout 代替。

if (!window.requestAnimationFrame) {
  requestAnimationFrame = function(fn) {
    setTimeout(fn, 17);
  };
}

写个例子,熟悉下

需求:点击按钮,显示进度条 效果图:

效果图.gif HTML 代码

<div id="test" style="width: 0px; height: 12px; line-height: 12px; margin-bottom: 5px; background: rgb(185, 236, 243);"></div>
当前进度:<span id="progress">0%</span>
<button id="btn">开启进度条</button>

使用 setInterval 实现

btn.onclick = function() {
  var timer = setInterval(function () {
    if (parseInt(test.style.width) < 300) {
      test.style.width = parseInt(test.style.width) + 3 + 'px';
      progress.innerHTML = parseInt(test.style.width) / 3 + '%';
    } else {
      clearInterval(timer);
    }
  }, 17);
}

使用 setTimeout 实现

btn.onclick = function() {
  var timer = setTimeout(function fn() {
    if (parseInt(test.style.width) < 300) {
      test.style.width = parseInt(test.style.width) + 3 + 'px';
      progress.innerHTML = parseInt(test.style.width) / 3 + '%';
      timer = setTimeout(fn, 17);
    } else {
      clearTimeout(timer);
    }
  }, 17);
}

使用 requestAnimationFrame 实现

btn.onclick = function() {
  var timer = requestAnimationFrame(function fn() {
    if (parseInt(test.style.width) < 300) {
      test.style.width = parseInt(test.style.width) + 3 + 'px';
      progress.innerHTML = parseInt(test.style.width) / 3 + '%';
      timer = requestAnimationFrame(fn);
    } else {
      cancelAnimationFrame(timer);
    }
  });
}

参考

写到最后

requestAnimationFrame 本质上是为了解决定时器时间间隔不稳定的问题。

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你或者喜欢,欢迎点赞和关注。