使用 requestAnimationFrame 实现高性能JS动画

我心飞翔 分类:javascript

在web开发中,我们通过js的实现动画一般是通过定时器(setTimeoutserInterval)。

定时器实现动画

示例

let animateTimer = setTimer(() => {
 // 动画逻辑......
 // if(...)  clearTimeout(animateTimer )  满足某种条件 清除该动画
},1000/60)
 

这里把显示器屏幕刷新率视为60Hz,即每次屏幕刷新时同步动画。避免因掉帧引起的卡顿感。

定时器实现动画的弊端

  • 1、setTimeoutsetInterval 分别有4毫秒和10毫秒的最小时间间隔,前面如果有复杂的js逻辑会让动画的间隔延迟更长。
  • 2、 使用定时器频繁操作DOM做动画,可能会造成 页面卡顿性能浪费动画掉帧 等问题

requestAnimationFrame(以下用RAF简称)

什么是 RAF

告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行------MDN
说白了,就是你调用RAF并传入一个回调函数,下次页面重绘就会执行传入的回调。

RAF 优势

  • requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
  • 在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。

清理RAF

如同定时器实现动画一样,某段动画执行完毕怎么清除它。cancelAnimationFrame就是用来清除RAF的方法,每个 requestAnimationFrame 调用都会返回这个动画对应的ID,将这个ID传入到 cancelAnimationFrame(RAFID) 即可清除指定动画

定时器动画与RAF动画对比

用这两种方式分别实现同一个动画,在页面一个盒子,开始从最左侧向右运动,碰到右侧边界开始向左运动,碰到左侧边界就开始向右侧运动不断重复这个动作。
定时器盒子在运动中,感觉有一定的卡顿感(掉帧)。RAF盒子感觉正常。

vu6k7-eitr9.gif

<template>
<div class="container">
<div class="canvas_container">
<div class="run_box1" ref="box1" :style="{ left: `${left1}px` }">
定时器
</div>
<div class="run_box2" ref="box2" :style="{ left: `${left2}px` }">RAF</div>
</div>
<button @click="hClickBTN">前进</button>
</div>
</template>
<script>
export default {
components: {},
mounted() {
this.VNOffset1 = this.$refs.box1.getBoundingClientRect()
this.VNOffset2 = this.$refs.box2.getBoundingClientRect()
},
data() {
return {
left1: 0,
step1: 1,
VNOffset1: {},
left2: 0,
step2: 1,
VNOffset2: {}
}
},
methods: {
runStart(screenWidth) {
setTimeout(() => {
const _this = this
if (_this.left1 <= 0 && _this.step1 !== 10) {
_this.step1 = 10
} else if (_this.left1 >= screenWidth - _this.VNOffset1.width && _this.step1 !== -10) {
_this.step1 = -10
}
_this.left1 += _this.step1
_this.runStart(screenWidth)
}, 1000 / 60)
},
animateStart(screenWidth) {
const animateID = requestAnimationFrame((curTime) => {
console.log("时间搓", curTime);
const _this = this
if (_this.left2 <= 0 && _this.step2 !== 10) {
_this.step2 = 10
} else if (_this.left2 >= screenWidth - _this.VNOffset2.width && _this.step2 !== -10) {
_this.step2 = -10
}
_this.left2 += _this.step2
_this.animateStart(screenWidth)
})
},
hClickBTN() {
const screenWidth = document.documentElement.clientWidth
this.runStart(screenWidth)
this.animateStart(screenWidth)
}
}
}
</script>  
<style scoped lang="scss">
.container {
height: 100%;
.canvas_container {
position: relative;
width: 750px;
height: 750px;
background-color: pink;
}
.run_box1,
.run_box2 {
position: absolute;
top: 0;
width: 200px;
height: 200px;
text-align: center;
line-height: 200px;
font-size: 40px;
color: #fff;
background-color: #000;
}
.run_box2 {
top: 400px;
}
button {
height: 60px;
line-height: 60px;
padding: 0 30px;
font-size: 25px;
border-radius: 10px;
background-color: powderblue;
color: #fff;
margin: 30px auto;
display: block;
}
}
</style>

回复

我来回复
  • 暂无回复内容