上图是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个步骤来呈现新外观:
-
Layout: 重新计算每个元素的尺寸信息和位置
-
Paint: 重新计算具体位置的样式信息,包括背景,颜色
-
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