一、requestAnimationFrame 的定义

官方的定义:

requestAnimationFrame 即请求动画帧,它是一个浏览器的宏任务。简单的说,这个api主要是用来做动画的。

二、关于前端动画的两个问题

1. 前端动画方案有哪些?

  • css动画

    • transition:过渡动画
    • animation:直接动画(搭配@keyframes
  • js动画

    • setIntervalsetTimeout定时器(比如不停地更改dom元素的位置,使其运动起来)
    • canvas动画,搭配js中的定时器去运动起来(canvas只是一个画笔,然后我们通过定时器会使用这个画笔去画画-动画)
    • requestAnimationFrame动画(js动画中的较好方案)

2. 为何要使用这个新的api来做动画?

在工作中,做动画最优的方案无疑是css动画,但是某些特定场景下,css动画无法实现我们所需要的需求。这时,我们就要考虑使用js去做动画了,canvas动画本质也是定时器动画。使用定时器动画干活,实际上是可以的,但是存在一个最大的问题,就是动画会抖动,体验效果不是非常好。

而使用requestAnimationFrame去做动画,就不会出现抖动的现象。

这里笔者写一个demo动画(分别是上述两种方式实现dom元素向右平移)给大家看一下,就知道具体的区别。我们先看一下效果图:

由于gif录制软件的问题,看着都有点卡。实际上,大家把下方代码复制一份跑起来看的话,会发现定时器动画在微微颤抖,而requestAnimationFrame动画却稳如老狗

 1<!DOCTYPE html>
 2<html lang="en">
 3
 4  <head>
 5    <meta charset="UTF-8">
 6    <meta http-equiv="X-UA-Compatible" content="IE=edge">
 7    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 8    <title>requestAnimationFrame_yyds</title>
 9    <style>
10      body {
11        box-sizing: border-box;
12        background-color: #ccc;
13      }
14
15      .box1,
16      .box2 {
17        position: absolute;
18        width: 160px;
19        height: 160px;
20        line-height: 160px;
21        text-align: center;
22        color: #fff;
23        font-size: 13px;
24      }
25
26      .box1 {
27        top: 40px;
28        background: red;
29      }
30
31      .box2 {
32        top: 210px;
33        background: green;
34      }
35    </style>
36  </head>
37
38  <body>
39    <button class="btn">👉 let's go!</button>
40    <div class="box1">定时器动画</div>
41    <div class="box2">请求动画帧</div>
42    <script>
43      // 动画思路:不断修改dom元素的left值,使其运动起来(动画)
44      let box1 = document.querySelector('.box1')
45      let box2 = document.querySelector('.box2')
46
47      // setInterval定时器方式
48      function setIntervalFn(
49
50      ) {
51        let timer = null
52        box1.style.left = '0px'
53        timer = setInterval(() => {
54          let leftVal = parseInt(box1.style.left)
55          if (leftVal >= 720) {
56            clearInterval(timer)
57          } else {
58            box1.style.left = leftVal + 1 + 'px'
59          }
60        }, 17)
61      }
62
63      // requestAnimationFrame请求动画帧方式
64      function requestAnimationFrameFn(
65
66      ) {
67        let timer = null // 可注掉
68        box2.style.left = '0px'
69        function callbackFn(
70
71        ) {
72          let leftVal = parseInt(box2.style.left)
73          if (leftVal >= 720) {
74            // 不再继续递归调用即可,就不会继续执行了,下面这个加不加都无所谓,因为影响不到
75            // cancelAnimationFrame取消请求动画帧,用的极少,看下,下文中的回到顶部组件
76            // 大家会发现并没有使用到这个api(这样写只是和clearInterval做一个对比)
77            // 毕竟,正常情况下,requestAnimationFrame会自动停下来
78            cancelAnimationFrame(timer) // 可注掉(很少用到)
79          } else {
80            box2.style.left = leftVal + 1 + 'px'
81            window.requestAnimationFrame(callbackFn)
82          }
83        }
84        window.requestAnimationFrame(callbackFn)
85      }
86
87      // 动画绑定
88      let btn = document.querySelector('.btn')
89      btn.addEventListener('click', () => {
90        setIntervalFn()
91        requestAnimationFrameFn()
92      })
93    </script>
94  </body>
95
96</html>

通过上述的例子,我们可以回答这个问题了:

  • 面试官问:requestAnimationFrame比定时器好在哪里?
  • 候选人答:好在比较稳定,动画不卡顿
  • 面试官说:你回去等通知吧…

所以在这里,我们还要顺带延伸一下,为什么定时器会卡,而requestAnimationFrame不会卡。在说这个问题之前,我们先来看下requestAnimationFrame的语法规则。

三、requestAnimationFrame的语法规则

requestAnimationFramejs中的setTimeout定时器函数基本一致,不过setTimeout可以自由设置间隔时间,而requestAnimationFrame的间隔时间是由浏览器自身决定的,大约是17毫秒左右

1.requestAnimationFrame我们可以在控制台输入window,然后展开查看其身上的属性,就能找到了,如下图:

xxx.png

xxx.png

2.由上图我们可以看到,requestAnimationFrame本质上是一个全局window对象上的一个属性函数,所以我们使用时,直接:window.requestAnimationFrame(callBack)即可。

3.和定时器一样其接收的参数callback也是一个函数,即下一次重绘之前更新动画帧所调用的函数,即在这个函数体中,我们可以写对应的逻辑代码(和定时器类似)。

4.requestAnimationFrame也有返回值,返回值是一个整数,主要是定时器的身份证标识,可以使用 window.cancelAnimationFrame()来取消回调函数执行,相当于定时器中的clearTimeout()

5.二者也都是只执行一次,想要继续执行,做到类似setInterval的效果,需要写成递归的形式(上述案例中也提到了)

四、关于卡顿的问题

1. 为什么定时器会卡

  • 我们在手机或者电脑显示屏上看东西时,显示屏会默默的不停地干活(刷新画面)
  • 这个刷新值得是每秒钟刷新次数,普通显示器的刷新率约为60Hz(每秒刷新60次),高档的有75Hz、90Hz、120Hz、144Hz等等
  • 刷新率次数越高,显示器显示的图像越清晰、越流畅、越丝滑
  • 不刷新就是静态的画面,刷新比较低就是卡了PPT的感觉
  • 动画想要丝滑流畅,需要卡住时间点进行代码操作(代码语句赋值、浏览器重绘)
  • 所以只需要每隔1000毫秒的60分之一(60HZ)即约为17毫秒,进行一次动画操作即可
  • 只要卡住这个17毫秒,每隔17毫秒进行操作,就能确保动画丝滑
  • 但是定时器的回调函数,会受到js的事件队列宏任务、微任务影响,可能设定的是17毫秒执行一次,但是实际上这次是17毫秒、下次21毫秒、再下次13毫秒执行,所以并不是严格的卡住了这个60HZ的时间
  • 没有在合适的时间点操作,就会出现:类似这样的情况:不变不变不变
  • 于是就出现了,绘制不及时的情况,就会有抖动的出现(以上述案例,位置和时间没有线性对应更新变化导致看起来抖动)

2. 为何requestAnimationFrame不会卡

setTimeoutsetInterval的问题是,它们都不精确。它们的内在运行机制决定了时间间隔,参数实际上只是指定了把动画代码添加到浏览器UI线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。

requestAnimationFrame能够做到,精准严格的卡住显示器刷新的时间,比如普通显示器60HZ它会自动对应17ms执行一次,高级显示器120HZ,它会自动对应9ms执行一次。当然requestAnimationFrame只会执行一次,想要使其多次执行,要写成递归的形式。

所以,这就是requestAnimationFrame的好处,window.requestAnimationFrame这个api就是解决了定时器不精准的问题的。

五、requestAnimationFrame 的应用场景

比如:回到顶部组件,就是使用requestAnimationFrame实现的。

效果图:

代码:

 1<template>
 2  <transition name="fade-transform">
 3    <div
 4         v-show="visible"
 5         class="backWrap"
 6         :style="{
 7                 bottom: bottom + 'px',
 8                 right: right + 'px',
 9                 }"
10         @click="goToTop"
11         >
12      <slot></slot>
13    </div>
14  </transition>
15</template>
16
17<script>
18
19  export default {
20    name: "myBack",
21    props: {
22      bottom: {
23        type: Number,
24        default: 72,
25      },
26      right: {
27        type: Number,
28        default: 72,
29      },
30      // 回到顶部出现的滚动高度位置
31      showHeight: {
32        type: Number,
33        default: 240,
34      },
35      // 拥有滚动条的那个dom元素的id或者class,用于下方选中操作更改滚动条滚动距离
36      scrollBarDom: String,
37    },
38    data(
39
40    ) {
41      return {
42        visible: false,
43        scrollDom: null,
44      };
45    },
46    mounted(
47
48    ) {
49      if (document.querySelector(this.scrollBarDom)) {
50        this.scrollDom = document.querySelector(this.scrollBarDom);
51        // 不用给window绑定监听滚动事件,给对应滚动条元素绑定即可
52        this.scrollDom.addEventListener("scroll", this.isShowGoToTop, true);
53      }
54    },
55    beforeDestroy(
56
57    ) {
58      // 最后要解除监听滚动事件
59      this.scrollDom.removeEventListener("scroll", this.isShowGoToTop, true);
60    },
61    methods: {
62      isShowGoToTop(
63
64      ) {
65        // 获取滚动的元素,即有滚动条的那个元素
66        if (this.scrollDom.scrollTop > 20) {
67          this.visible = true;
68        } else {
69          this.visible = false;
70        }
71      },
72      goToTop(
73
74      ) {
75        // 获取滚动的元素,即有滚动条的那个元素
76        let scrollDom = document.querySelector(this.scrollBarDom);
77        // 获取垂直滚动的距离,看看滚动了多少了,然后不断地修改滚动距离直至为0
78        let scrollDistance = scrollDom.scrollTop;
79
80        /**
81 * window.requestAnimationFrame兼容性已经可以了,正常都有的
82 * */
83        if (window.requestAnimationFrame) {
84          let fun = (
85
86          ) => {
87            scrollDom.scrollTop = scrollDistance -= 36;
88            if (scrollDistance > 0) {
89              window.requestAnimationFrame(fun); // 只执行一次,想多次执行需要再调用
90            } else {
91              scrollDom.scrollTop = 0;
92            }
93          };
94          window.requestAnimationFrame(fun);
95          return;
96        }
97
98        /**
99 * 没有requestAnimationFrame的话,就用定时器去更改滚动条距离,使之滚动
100 * */
101        let timer2 = setInterval(() => {
102          scrollDom.scrollTop = scrollDistance -= 36;
103          if (scrollDistance <= 0) {
104            clearInterval(timer2);
105            scrollDom.scrollTop = 0;
106          }
107        }, 17);
108      },
109    },
110  };
111</script>
112
113<style lang='less' scoped>
114
115  .backWrap {
116    position: fixed;
117    cursor: pointer;
118    width: 42px;
119    height: 42px;
120    background: #9cc2e5;
121    border-radius: 4px;
122    display: flex;
123    justify-content: center;
124    align-items: center;
125    transition: all 0.5s;
126  }
127
128  // 过渡效果
129  .fade-transform-leave-active,
130  .fade-transform-enter-active {
131    transition: all 0.36s;
132  }
133
134  .fade-transform-enter {
135    opacity: 0;
136    transform: translateY(-5px);
137  }
138  .fade-transform-leave-to {
139    opacity: 0;
140    transform: translateY(5px);
141  }
142</style>
个人笔记记录 2021 ~ 2025