使用 requestAnimationFrame 实现高性能JS动画

·  阅读 473

在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>
复制代码
分类:
前端
标签:
分类:
前端
标签: