前言
都 2022 年了,还不在页面中加点动画吗?
合理高效的动画会让用户感觉的体验与交互更好,在 web 中,常常使用 CSS 动画(transition 与 keyFrames)、SVG 动画(SMIL, JavaScript, CSS)、JavaScript 来实现动画。
其中,JavaScript 因为可控制的细节更多,可完成的效果也更多。
按照旧有的方式,通常在使用 JavaScript 做动画时,会使用 setTimeout/setInterval 来完成动画效果,比如旧版本的 jQuery 中的 animate 方法(新版本的已经使用requestAnimationFrame)。
但是,都 2022 年了,放过 setTimeout/setInterval 吧,它们真的不适合做这个。
而requestAnimationFrame 这个 API,看名字就知道是为了动画而生的,但它作为Web API,只要需求符合,一样可以利用其特性完成效果。
让我们一起来了解一下。
动画帧
无论用什么来实现动画,运用的原理都是相近的。
人眼有短暂的视觉残留,即当人眼看到一个图像信息后,这个画面会短暂的停留在视网膜上一段时间。
早期电影或动画行业常用的就是 24 帧,即 1s 播放 24 个一连串的画面,每一个画面就是 1 帧。但实际来说,帧率(FPS)越高,实际感觉就会越流畅。
在 web 上并没有什么不同,web 动起来所要的帧(画面)也是相近的,但在 web 上通常不会选择 24 作为帧率,而是采用浏览器的刷新率来作为帧率。
通常情况下这一值是 60 帧,即我们需要在 1000/60≈16.7ms 内完成一帧的绘制。
为什么不要用 setTimeout/setInterval 来完成动画
有了时间,有了要做的目标(绘制新画面,通常来说就是操作元素),很容易就会想到使用setTimeout/setInterval API 来做。
但最大最大的问题是,setTimeout/setInterval 的 delay 值并不精确,它并不能保证代码在指定的时间去执行代码,MDN 甚至直接将其写进了参数说明:
不管是哪种情况,实际的延迟时间可能会比期待的(delay 毫秒数) 值长
这对于动画来说是致命的,肉眼很容易感知出这种“不流畅”。
具体的原因需要了解浏览器的 Event-loop 机制与 DelayTask,但不管怎样,它们执行的时机都是不准确的。
那么把 delay 值改小一些可以吗?
当然也是不行的,例如设为 10ms 更新画面,这样在实际显示时必定会忽略某画面,造成跳帧(某画面肉眼并没有看到)。
那么有没有可以准确执行更新画面的 API 呢?当当当当当,主角 requestAnimationFrame 出马了。
为什么用 requestAnimationFrame 更好
与刷新率吻合的时间
setTimeout/setInterval 的时间不准确,而 requestAnimationFrame 的时间可以说非常准确。
它执行的时机完全取决于浏览器重绘的时机,它的回调函数会在下次浏览器重绘之前执行。
大部分的浏览器刷新率都是 60hz, 但近些年来高刷(75hz, 90hz, 120hz, 144hz, 240hz)显示设备在快速普及,requestAnimationFrame 会根据显示设备的刷新率来运行,可以看如下测试:
注意 requestAnimationFrame 的回调中会有一个参数,它是一个DOMHighResTimeStamp 类型值,所以时间差可以到小数位。
当 startLog 时,实测在 60hz 的显示设备下,log 的值通常是 16.683 左右,在 120hz 的显示设备下,log 的值通常为 8.3 左右,时间值基本都与刷新率所吻合。
避免无效的资源浪费
requestAnimationFrame 运行在重绘之前,所以它会将每一帧的 DOM 操作集中起来,在一次中回流或重绘中完成。这意味着即使在不同位置同时触发了多次 requestAnimationFrameTime 回调,更新也只会在一帧中进行。这一点也可以在回调函数的参数中得以验证:
在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳,即使在计算上一个回调函数的工作负载期间已经消耗了一些时间。
当页面不是激活状态时,requestAnimationFrame 会自动停止,也会节省资源开销。
requestAnimationFrame 的其他用处
给事件添加节流或防抖
某些事件在浏览器中需要做节流或者防抖,这时使用requestAnimationFrame 替代setTimeout 可以获得更好的性能。
类似下面的防抖函数,
function rafThrottle(func) {
let lock = false;
return function (...args) {
if (lock) return;
lock = true;
window.requestAnimationFrame(() => {
func.apply(this, args);
lock = false;
});
};
}
时间切片(Time Slicing)
JavaScript 作为单线程语言,当运行长任务时会阻塞其他行为,在web 上来看就像是页面假死。
当延迟超过100ms,用户就会察觉到延迟感。
为了避免这种情况,通常会采用Web Worker 或者时间切片(Time Slicing)。
Web Worker 采用了单独开线程而不阻塞用户行为的方式来进行任务,这里不再细说。
而时间切片是采用分割长任务的方式来解决这一问题,限制任务的可执行时间,从而不影响用户交互。