本文将给大家介绍一个使用 js 实现动画的利器,requestAnimationFrame,我们一般情况下,在 js 实现一个动画,一般是使用 setInterval 实现,不过通常使用这个方法实现动画的时候,细看能够看见一些抖动感
requestAnimationFrame()
。因为 requestAnimationFrame()
是一次性的cancelAnimationFrame
方法这个原因可能有不少,但是主要的就是 浏览器渲染帧的不同步
为什么会不同步呢?主要原因是浏览器无法确定定时器的回调函数的执行时机
我们知道 setInterval 是一个异步的任务,只有当同步任务执行完成之后才会执行异步任务,假设我们设置的 setInterval 是 20ms 执行一次,那么如果同步代码执行的时间是 10ms,也就表示实际上 setInterval 执行的时候已经隔了 30ms 的时间了
这是一点,那我们在猜想一下,按照 60 帧率来计算的话,应该是 16.7ms 执行一次,就可以达到一个比较细腻的动画,那么如果我们将 setInterval 的时间设置为 16.7ms 可以解决这个问题吗,也是不行的,还是这个异步的问题,加上等待同步代码执行时间,这个时间一定是不精确的,何况本身浏览器的计时存在细小的误差
为了方便理解,我们还是回到这个开始设置 20ms 的时间,按照预期 60 帧渲染的话,那么不算其他干扰的情况,那么执行的时间线如下:
可以看到,第一帧没有渲染,20ms的时候执行了 setInterval 代码了,但是就需要等待下一次的执行时机,也就第二帧,而如果假设是 10ms 的话,那么第一帧可以找到,但是在执行第二帧的时候,其实 setInterval 执行了三次,也就是说在第二帧本该执行第二次 setInterval 所设置的动画数值,变了第三次 setInterval 设置的动画数值,出现了一次跳跃,那自然看起来也会存在抖动感
正是因为由于这种不同步,就导致了一次执行渲染一针或者多帧,或者当前帧没有渲染,下一次执行的时候数值跳跃等等,从而导致动画存在抖动感
这个更加具体一点大家可以参考手绘动画翻页
这种效果,如果翻到某一页突然停一下,或者某两页或者几页黏在一起,被当做一页翻过去的时候,就可以名显的感觉到这个不连贯的感觉
或者有人说,我可以强制触发reflow来触发啊,那还是这个问题,屏幕的刷新率是固定的,你重绘时机过早也不会被展示出来,也不会被用户感知到,等于无效的操作,那么还是无法解决这个问题
相信在经过上面 setInterval 实现动画为什么存在抖动感的解析,就知道应该要怎么解决,解决方案就是在每一帧渲染之前,就处理好每一帧需要表现的动画样式和一些数据,然后就可以在每一帧时机渲染的时候处理正确的动画效果,这样就可以保证我们的动画拥有相对细腻的效果了
而 requestAnimationFrame 就可以帮助我们完成这个效果
requestAnimationFrame 是只会执行一次的,所以我们如果需要利用 requestAnimationFrame 实现动画效果的话,往往需要递归,也就是如下的形式,如下:
function run(){
requestAnimationFrame(run)
}
requestAnimationFrame(run)
实现代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>requestAnimationFrame</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.sign {
width: 500px;
height: 220px;
border-right: 2px solid #000;
position: relative;
margin-left: 20px;
}
.box {
width: 100px;
height: 100px;
background-color: salmon;
position: absolute;
left: 0;
top: 0;
}
.box1 {
top: 120px;
background-color: skyblue;
word-wrap: break-word;
}
.btns {
margin-left: 20px;
margin-top: 30px;
}
</style>
</head>
<body>
<div class="sign">
<div class="box">setInterval</div>
<div class="box box1">requestAnimationFrame</div>
</div>
<div class="btns">
<button class="s-btn">setInterval</button>
<button class="r-btn">requestAnimationFrame</button>
<button class="btn">一起执行</button>
</div>
<script>
const box = document.querySelector('.box');
const box1 = document.querySelector('.box.box1');
const sBtn = document.querySelector('.s-btn')
const rBtn = document.querySelector('.r-btn')
const btn = document.querySelector('.btn')
let boxLeft = 0
let boxId = null
let box1Left = 0
let box1Id = null
sBtn.addEventListener('click', () => {
boxId = setInterval(() => {
boxLeft++
box.style.left = boxLeft + 'px'
if (boxLeft >= 400) {
clearInterval(boxId)
boxId = null
}
}, 1000 / 60)
})
function run() {
box1Left++
box1.style.left = box1Left + 'px'
if (box1Left >= 400) {
cancelAnimationFrame(box1Id)
box1Id = null
return
}
requestAnimationFrame(run)
}
rBtn.addEventListener('click', () => {
box1Id = requestAnimationFrame(run)
})
btn.addEventListener('click', () => {
sBtn.click()
rBtn.click()
})
</script>
</body>
</html>
屏幕刷新率为 60hz 时效果
屏幕刷新率为 165hz 时
由于这种 gif 的原因,可能实际感受不如在屏幕上看的明显,但是可以大致感知出来 setInterval 的那种抖动感,而且 setInterval 只能设置固定的时间,是无法契合当前的屏幕的刷新率的
这里在补充一点性能上的区别,这个就先了解一下他们在后台的运行机制:
requestAnimationFrame() 运行在后台标签页或者隐藏的 <iframe> 里时,
requestAnimationFrame()` 会被暂停调用以提升性能和电池寿命
而 setInterval 在后台也是不会停止调用的会继续在浏览器的内存中调用,消耗性能,比如最开始编写轮播图的时候,如果页面隔一段时间切换回来之后,会导致轮播图切换的非常快,就是因为没有停止执行,而切换回来之后,把失去的值都补上,这个也是可以验证的,比如我们把 setInterval 执行改为 2000ms,每次增加 50px,来看一下效果,如图
通过这个是可以看到切换回来的时候,一下子就跳跃了一大段距离,就表示实际是没有停止执行的,一直改变着移动的数值,切换胡来的时候才会进行了跳跃式的移动
当然动画最优选还是 css3,不过有些动画总会需要 js 的介入嘛