一、requestAnimationFrame 的定义
官方的定义:

requestAnimationFrame
即请求动画帧,它是一个浏览器的宏任务。简单的说,这个api主要是用来做动画的。
二、关于前端动画的两个问题
1. 前端动画方案有哪些?
-
css
动画transition
:过渡动画animation
:直接动画(搭配@keyframes
)
-
js
动画setInterval
或setTimeout
定时器(比如不停地更改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的语法规则
requestAnimationFrame
和js
中的setTimeout
定时器函数基本一致
,不过setTimeout
可以自由设置间隔时间,而requestAnimationFrame
的间隔时间是由浏览器自身决定的,大约是17毫秒
左右
1.requestAnimationFrame
我们可以在控制台输入window
,然后展开查看其身上的属性,就能找到了,如下图:
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
不会卡
setTimeout
和setInterval
的问题是,它们都不精确。它们的内在运行机制决定了时间间隔,参数实际上只是指定了把动画代码添加到浏览器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>