浅谈 RequestAnimationFrame

浅谈 RequestAnimationFrame

上图是MDN官方文档解释,我这里用一句话概括,requestAnimationFrame这个api主要是用来做动画的。

那么,前端动画方案有很多,为什么偏要有它呢?

在了解它之前,我们来重新认识一下前端的动画方案,如果有遗漏的地方,欢迎在评论区里提及。

前端动画主要分为 CSS 动画 和 JS动画

CSS 动画

CSS动画使得在没有 JavaScript 的情况下制作简单的动画成为可能

transition

过渡动画,其属性为

释义 默认值
transition-property 指定应用过渡属性的名称 all
transition-duration 过渡动画所需的时间 0s
transition-timing-function 过渡的速度曲线 ease
transition-delay 等待时间 0s

tiransition 动画(rotate,flip,stretch,shift)在性能方面有优势

  • 将整个目标元素作为一个整体,不考虑整体内部的细节
  • 不会影响到周围的元素
  • 浏览器可以充分利用图像加速器(CPU或图形卡上的特殊芯片)

animation

直接动画(搭配@keyframes)

释义 默认值
@keyframes 动画名称(运动的规则)
animation-name @keyframes 动画的名称
animation-duration 动画花费时间 0s
animation-timing-function 动画的速度曲线 ease
animation-delay 等待时间 0s
animation-iteration-count 播放次数 1
animation-direction 是否在下一周期逆向地播放 normal
animation-fill-mode 控制动画执行前和执行后的状态
animation-play-state 动画是否正在运行或暂停 running

性能

大多数CSS属性都可以被动画化,因为它们大多数都是数值。例如:width、color、font-size 都是数字。设置动画时,浏览器会逐帧逐渐更改这些数字,从而创建平滑效果,但不同的CSS属性的更改成本不同。

当有样式更改时,浏览器会通过3个步骤来呈现新外观:

  1. Layout: 重新计算每个元素的尺寸信息和位置

  2. Paint: 重新计算具体位置的样式信息,包括背景,颜色

  3. Composite: 将最终结果渲染为屏幕上的像素,如果存在CSS transforms,将其作用到对应元素上。

但也存在跳过步骤的情况,比如 color,如果 color 发生更改,浏览器不会计算任何新几何体,而是转到Paint→Composite。而且很少有属性直接进入 Composite。传送门

计算需要时间,特别是在包含许多元素和复杂布局的页面上,延迟实际上在大多数设备上都是可见的,导致抖动,不那么流畅的动画,这里也是 CSS 动画的痛点。

这里需要提一下 transforms,因为

  • CSS transforms 将整个目标元素作为一个整体,不考虑整体内部的细节
  • CSS transforms 永远不会影响相邻元素
  • transform 属性的更改(动画)永远不会触发 layout 和 paint 步骤

浏览器计算布局(大小,位置),在 Paint 阶段用颜色,背景等绘制它,然后将 transform 应用于其元素上。

另外需要提一下 opacity 属性,它也从不触发 layout 阶段。

通常使用 transforms + opacity 就可以实现我们日常的使用。

JS 动画

JavaScript动画可以处理CSS不能处理的事情,比如复杂的运动轨迹,与其他元素进行联动,或者创建新元素。

🤦‍♂️setInterval

一个动画可以被实现为一系列的帧,利用视觉停留效应,通俗易懂的说,就是和电影院里的原理是一样的,每秒24帧让它看起来很流畅

我们用一个例子来说明:将 style.left 从 0px 更改为 100px 会移动元素

const start = Date.now(); // remember start time

const timer = setInterval(function() {
  // how much time passed from the start?
  const timePassed = Date.now() - start;

  if (timePassed >= 2000) {
    clearInterval(timer); // finish the animation after 2 seconds
    return;
  }

  // draw the animation at the moment timePassed
  draw(timePassed);
}, 20);

// as timePassed goes from 0 to 2000
// left gets values from 0px to 400px
function draw(timePassed) {
  train.style.left = timePassed / 5 + 'px';
}

我们可以从代码中看出使用 setInterval 后的弊端

  • 若有多个 setInterval 实现的动画,可能存在多个废弃的帧
  • 固定的时间间隔不能灵活的配合 CPU 的工作,如果 CPU 过载,或者有其他原因不经常重绘,那么更新频率就不会那么快,从而造成动画抖动或者卡顿的现象。
// 起始计算时间不同,会导致多个废弃的帧
setInterval(animate1, 20); // independent animations
setInterval(animate2, 20); // in different places of the script
setInterval(animate3, 20);

/*
这几个独立的重画应该组合在一起,使重画更容易为浏览器,从而加载更少的CPU负载,看起来更流畅
但是固定的时间间隔不能灵活的配合 CPU 的工作,如果 CPU 过载,那么更新频率就不会那么快
*/
setInterval(function() {
  animate1();
  animate2();
  animate3();
}, 20)

这时,requestAnimationFrame 就该登场了

👍 requestAnimationFrame

const requestId = requestAnimationFrame(callback)

// cancel the scheduled execution of callback
cancelAnimationFrame(requestId);



/*
  callback 存在一个参数,即从页面加载开始经过的时间(毫秒)。
  这个时间也可以通过调用 performance.now() 获得。
*/
let prev = performance.now();
let times = 0;

requestAnimationFrame(function measure(time) {
  document.body.insertAdjacentHTML("beforeEnd", Math.floor(time - prev) + " ");
  prev = time;

  if (times++ < 10) requestAnimationFrame(measure);
})

浏览器将 callback 函数安排在其想要绘制动画的时机执行

  • 即使存在多个分开的 requestAnimationFrame ,它们的 callback 都是在浏览器要绘制动画最近的时间点执行,把这些 callback 集中在一起执行,就不会有多次废弃的 layout 和 paint

  • 浏览器会在空闲阶段执行 callback,cpu 过载是更新频率自然就没有那么快了

requestAnimationFrame 和 JS 中的 setTimeout 定时器函数基本一致,不过 setTimeout 可以自由设置间隔时间,而requestAnimationFrame 的间隔时间是由浏览器自身决定的,大约是17毫秒左右

if (!window.requestAnimationFrame) {
	window.requestAnimationFrame = (callback) => {
    setTimeout(callback, 17)
  }
}

与setTimeout相比,requestAnimationFrame最大的优势是由浏览器来决定回调函数的执行时机,即紧跟浏览器的刷新步调。

具体一点讲,如果屏幕刷新频率是60Hz,那么回调函数每16.7ms被执行一次,如果屏幕刷新频率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,自然不会导致动画的卡顿。

原文链接:https://juejin.cn/post/7336887570976210983 作者:youwne

(0)
上一篇 2024年2月19日 下午5:04
下一篇 2024年2月19日 下午5:14

相关推荐

发表回复

登录后才能评论