下面展示分别展示在setTimeout和requestAnimationFrame中制作执行动画的方法(代码在后面)
Settimeout
打开控制台,然后找这个performance,上面的图是settimeout在性能面板执行出来的情况下,可以看到一帧可能被触发两次定时器,也有可能一帧的时候一个定时器都不触发
在这种情况下两个帧只执行了一次settimeout,就会出现卡顿的效果,导致动画不流畅,用户感觉的卡顿感呢或者是有跳跃、有卡顿。
settimeout现在已经不适合用做动画了啊。正确做法,是使用下边的API
requestAnimationFrame
当使用 requestAnimationFrame 做动画时,其回调函数的时间间隔并不是固定的。我们不能假设每次回调都会严格按照 16.6 毫秒的间隔触发,因为帧率可能会波动。
帧率的不稳定会导致回调函数的触发时机也随之变化。如果帧率不稳定,比如忽快忽慢,回调的间隔时间也会随之变化。但是requestAnimationFrame 它能够确保每次触发时都绘制一次画面,保证动画的每一帧都可见,并且每一帧只绘制一次。
requestAnimationFrame 的触发间隔取决于帧率,虽然帧率可能不稳定,但它能够最大程度地协调帧率与绘制频率,确保动画的平滑性和准确性。
简单来说就是会根据帧率的变化去执行,帧率变化执行动画的时机也就会变化,一帧执行一次动画。
那既然帧率也是不稳定的,应该如何去执行一个动画
先上一下代码案例
```js
<template>
<div>
<!-- setTimeout 小球 -->
<div
class="ball"
:style="{ transform: `translateX(${positionTimeout}px)` }"
></div>
<button @click="startAnimationTimeout">Start Animation (setTimeout)</button>
<!-- requestAnimationFrame 小球 -->
<div
class="ball"
:style="{ transform: `translateX(${positionRaf}px)` }"
></div>
<button @click="startAnimationRaf">Start Animation (requestAnimationFrame)</button>
</div>
</template>
<script>
export default {
data() {
return {
positionTimeout: 0, // setTimeout 小球的当前位置
positionRaf: 0, // requestAnimationFrame 小球的当前位置
animationId: null // 用于控制 requestAnimationFrame 动画的 ID
};
},
methods: {
// 使用 setTimeout 实现动画
startAnimationTimeout() {
this.positionTimeout = 0; // 重置位置
const step = () => {
if (this.positionTimeout < 300) {
this.positionTimeout += 5; // 每次移动 5px
setTimeout(step, 16); // 每 16ms 执行一次
}
};
step();
},
// 使用 requestAnimationFrame 实现动画
startAnimationRaf() {
this.positionRaf = 0; // 重置位置
const step = () => {
if (this.positionRaf < 300) {
this.positionRaf += 5; // 每帧移动 5px
this.animationId = requestAnimationFrame(step); // 请求下一帧
} else {
cancelAnimationFrame(this.animationId); // 动画结束后停止
}
};
step();
}
}
};
</script>
<style>
/* 小球样式 */
.ball {
width: 50px;
height: 50px;
background-color: red;
border-radius: 50%;
position: absolute;
top: 100px;
margin-bottom: 20px;
transition: transform 0.1s linear;
}
/* 第二个小球 */
.ball:nth-child(3) {
background-color: blue;
top: 200px;
}
button {
margin-top: 120px;
margin-left: 10px;
}
</style>
利用api + 速度
我们需要确定一个运动的速度,比如小球的运动每秒移动多少像素,在动画开始时,记录一个时间点,作为基准时间,在每一帧更新时,计算从起始时间到当前时间的时间差,通过公式:
当前位置 = 起始位置 + 速度 × 时间差
计算出物体当前应该的位置,将计算得出的当前位置更新到动画元素上,从而实现平滑的运动
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>小球动画示例</title>
<style>
/* 容器样式 */
#container {
width: 400px;
height: 200px;
border: 1px solid black;
position: relative;
overflow: hidden;
}
/* 小球样式 */
#ball {
width: 20px;
height: 20px;
background-color: red;
border-radius: 50%;
position: absolute;
top: 90px; /* 垂直居中 */
left: 0; /* 初始位置 */
}
</style>
</head>
<body>
<!-- 容器 -->
<div id="container">
<div id="ball"></div>
</div>
<!-- 按钮 -->
<button id="start">开始动画</button>
<script>
const ball = document.getElementById('ball'); // 获取小球元素
const startButton = document.getElementById('start'); // 获取按钮元素
const containerWidth = 400; // 容器宽度
const ballWidth = 20; // 小球宽度
const speed = 100; // 小球每秒移动的像素
let startTime = null; // 动画的开始时间
let animationId = null; // 动画的 ID,方便停止动画
// 动画函数
function animate(timestamp) {
if (!startTime) {
startTime = timestamp; // 初始化开始时间
}
// 计算经过的时间(单位:秒)
const elapsedTime = (timestamp - startTime) / 1000;
// 计算小球的新位置
const currentPosition = Math.min(elapsedTime * speed, containerWidth - ballWidth);
// 更新小球的位置
ball.style.left = `${currentPosition}px`;
// 如果小球未到达终点,继续下一帧动画
if (currentPosition < containerWidth - ballWidth) {
animationId = requestAnimationFrame(animate);
} else {
cancelAnimationFrame(animationId); // 停止动画
}
}
// 点击按钮启动动画
startButton.addEventListener('click', () => {
startTime = null; // 重置开始时间
ball.style.left = '0px'; // 重置小球位置
cancelAnimationFrame(animationId); // 停止之前的动画
animationId = requestAnimationFrame(animate); // 启动新的动画
});
</script>
</body>
</html>
总结
setTimeout 的定时不是完全精确的,受到以下因素影响:
-
浏览器性能:如果浏览器忙于处理其他任务(如渲染或事件响应),
setTimeout的回调会被延迟执行。 -
系统计时器限制:浏览器通常有最小的时间间隔(通常是 4ms 或 10ms),在实际运行中可能不稳定。
-
累积误差:在一系列动画帧中,由于时间间隔的不准确,会导致动画速度变化不一致。
-
高效动画通常使用
requestAnimationFrame,它会在浏览器的 下一帧渲染周期 中执行。为了排除帧率不稳定的情况,常用速度的方法一起制作动画。