# requestAnimationFrame 官方介绍
# requestAnimationFrame 用处概述
window.requestAnimationFrame()
告诉浏览器 —— 你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行...
官方文档对应截图
官方文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
大致看了以后,我们可以知道:
requestAnimationFrame
这个 api
主要是用来做动画的。
requestAnimationFrame
这个 api
主要是用来做动画的。
requestAnimationFrame
这个 api
主要是用来做动画的。
其实顾名思义,我们翻译这个英文单词,也能大致明白。
request(请求)Animation(动画)Frame(帧)
# 关于前端动画的两个问题:
1. 前端动画方案有哪些?
2. 为何偏偏要使用这个新的 api 来做动画(或者说这个 api 较之前做动画的方式优点有哪些)?
# 1. 前端动画方案有哪些?
主要分类为 css动画
和 js动画
,如下细分:
css
动画transition
过渡动画animation
直接动画(搭配@keyframes
)
js
动画setInterval
或setTimeout
定时器(比如不停地更改dom元素
的位置,使其运动起来)canvas
动画,搭配js
中的定时器去运动起来(canvas
只是一个画笔,然后我们通过定时器会使用这个画笔去画画 - 动画)requestAnimationFrame动画(js动画中的较好方案)
另有
svg动画标签
,不过工作中这种方式是比较少的,这里不赘述
# 2. 为何偏偏要使用这个新的 api 来做动画(或者说这个 api 较之前做动画的方式优点有哪些)?
在工作中,做动画最优的方案无疑是 css动画
,但是某些特定场景下, css动画
无法实现我们所需要的需求,此时,我们就要考虑使用 js 去做动画了
canvas动画`的`本质`也是`定时器动画 |
使用定时器动画干活,实际上是可以的,但是存在一个最大的问题,就是 动画会抖动
、 动画会抖动
、 动画会抖动
,体验效果不是非常好。
而,使用 requestAnimationFrame
去做动画, 就不会抖动
、 就不会抖动
、 就不会抖动
这里笔者写一个 demo动画(分别是上述两种方式实现dom元素向右平移)
给大家看一下,就知道具体的区别。我们先看一下效果图:(红色 dom
是定时器实现、绿色 dom
是 requestAnimationFrame
实现)
因为笔者的 gif 录制软件的问题,看着都有点卡,实际上,大家把下方代码复制一份跑起来看的话,会发现定时器动画在微微颤抖,而
requestAnimationFrame
动画却稳如老狗
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>requestAnimationFrame_yyds</title> | |
<style> | |
body { | |
box-sizing: border-box; | |
background-color: #ccc; | |
} | |
.box1, | |
.box2 { | |
position: absolute; | |
width: 160px; | |
height: 160px; | |
line-height: 160px; | |
text-align: center; | |
color: #fff; | |
font-size: 13px; | |
} | |
.box1 { | |
top: 40px; | |
background: red; | |
} | |
.box2 { | |
top: 210px; | |
background: green; | |
} | |
</style> | |
</style> | |
</head> | |
<body> | |
<button class="btn"> let's go!</button> | |
<div class="box1">定时器动画</div> | |
<div class="box2">请求动画帧</div> | |
<script> | |
// 动画思路:不断修改 dom 元素的 left 值,使其运动起来(动画) | |
let box1 = document.querySelector('.box1') | |
let box2 = document.querySelector('.box2') | |
//setInterval 定时器方式 | |
function setIntervalFn() { | |
let timer = null | |
box1.style.left = '0px' | |
timer = setInterval(() => { | |
let leftVal = parseInt(box1.style.left) | |
if (leftVal >= 720) { | |
clearInterval(timer) | |
} else { | |
box1.style.left = leftVal + 1 + 'px' | |
} | |
}, 17) | |
} | |
//requestAnimationFrame 请求动画帧方式 | |
function requestAnimationFrameFn() { | |
let timer = null // 可注掉 | |
box2.style.left = '0px' | |
function callbackFn() { | |
let leftVal = parseInt(box2.style.left) | |
if (leftVal >= 720) { | |
// 不再继续递归调用即可,就不会继续执行了,下面这个加不加都无所谓,因为影响不到 | |
//cancelAnimationFrame 取消请求动画帧,用的极少,看下,下文中的回到顶部组件 | |
// 大家会发现并没有使用到这个 api(这样写只是和 clearInterval 做一个对比) | |
// 毕竟,正常情况下,requestAnimationFrame 会自动停下来 | |
cancelAnimationFrame(timer) // 可注掉(很少用到) | |
} else { | |
box2.style.left = leftVal + 1 + 'px' | |
window.requestAnimationFrame(callbackFn) | |
} | |
} | |
window.requestAnimationFrame(callbackFn) | |
} | |
// 动画绑定 | |
let btn = document.querySelector('.btn') | |
btn.addEventListener('click', () => { | |
setIntervalFn() | |
requestAnimationFrameFn() | |
}) | |
</script> | |
</body> | |
</html> |
Chrome 浏览器查看当前帧数命令:
1. F12打开控制台
、2. command + shift + p调出输入面板
、3. 在Run输入框中输入:Show frames per second(FPS) meter回车即可
通过上述的例子,我们可以回答这个问题了:
- 面试官问:
requestAnimationFrame
比定时器好在哪里? - 候选人答:好在比较稳定,动画不卡顿
- 面试官说:你回去等通知吧...
所以在这里,我们还要顺带延伸一下,为什么定时器会卡,而 requestAnimationFrame
不会卡。在说这个问题之前,这里再提一下, requestAnimationFrame
的语法规则
# requestAnimationFrame 的语法规则
一言以蔽之: requestAnimationFrame
和 js
中的 setTimeout
定时器函数 基本一致
,不过 setTimeout
可以自由设置间隔时间,而 requestAnimationFrame
的间隔时间是由浏览器自身决定的,大约是 17毫秒
左右
1. requestAnimationFrame
我们可以在控制台输入 window
,然后展开查看其身上的属性,就能找到了,如下图:
2. 由上图我们可以看到, requestAnimationFrame
本质上是一个全局 window
对象上的一个属性函数,函数是要被执行的,要被调用的。所以我们使时,直接: window.requestAnimationFrame(callBack)
即可。
3. 和定时器一样其接收的参数 callback
也是一个函数,即下一次重绘之前更新动画帧所调用的函数,即在这个函数体中,我们可以写对应的逻辑代码(和定时器类似)
4.requestAnimationFrame 也有返回值,返回值是一个整数,主要是定时器的身份证标识,可以使用 window.cancelAnimationFrame()来取消回调函数执行
,相当于定时器中的 clearTimeout()
。
5. 二者也都是只执行一次,想要继续执行,做到类似 setInterval
的效果,需要写成递归的形式(上述案例中也提到了)
# 为什么定时器会卡,而 requestAnimationFrame
不会卡
# 为什么定时器会卡
- 我们在手机或者电脑显示屏上看东西时,显示屏会默默的不停地干活(刷新画面)
- 这个刷新值得是每秒钟刷新次数,普通显示器的刷新率约为 60Hz(每秒刷新 60 次),高档的有 75Hz、90Hz、120Hz、144Hz 等等
- 刷新率次数越高,显示器显示的图像越清晰、越流畅、越丝滑
- 不刷新就是静态的画面,刷新比较低就是
卡了
,PPT
的感觉 - 动画想要丝滑流畅,需要卡住时间点进行代码操作(代码语句赋值、浏览器重绘)
- 所以只需要每隔 1000 毫秒的 60 分之一(60HZ)即约为 17 毫秒,进行一次动画操作即可
- 只要卡住这个 17 毫秒,每隔 17 毫秒进行操作,就能确保动画丝滑
- 但是定时器的回调函数,会受到 js 的事件队列宏任务、微任务影响,可能设定的是 17 毫秒执行一次,但是实际上这次是 17 毫秒、下次 21 毫秒、再下次 13 毫秒执行,所以并不是严格的卡住了这个 60HZ 的时间
- 没有在合适的时间点操作,就会出现:类似这样的情况:
变
、不变
、不变
、变
、不变
... - 于是就出现了,绘制不及时的情况,就会有抖动的出现(以上述案例,位置和时间没有线性对应更新变化导致看起来抖动)
js 执行代码是很快的,可能不到一毫秒,大家可以使用相应 console 的 api 去测试,如下:
console.time() | |
let box1 = document.querySelector('.box1') | |
box1.style.left = '100px' | |
console.timeEnd() | |
//js 执行耗时结果:default: 0.044189453125 ms |
# 为何 requestAnimationFrame
不会卡
requestAnimationFrame
能够做到,精准严格的卡住显示器刷新的时间,比如普通显示器 60HZ
它会自动对应 17ms
执行一次,比如高级显示器 120HZ
,它会自动对应 9ms
执行一次。
当然 requestAnimationFrame
只会执行一次,想要使其多次执行,要写成递归的形式。上述案例也给出了递归写法
至于为何
requestAnimationFrame
能够卡住时间,其底层原理又是啥?本文暂且按下不表。
所以,这就是 requestAnimationFrame
的好处。
所以,上述内容验证了:一项新技术新的技术方案的提出,一定是为了解决相关的问题的。
所以, window.requestAnimationFrame
这个 api
就是解决了定时器不精准的问题的。
这就是其产生的原因。
# requestAnimationFrame 应用场景举例 - 回到顶部组件
比如:回到顶部组件,就是使用 requestAnimationFrame
实现的。
下面是笔者封装的回到顶部组件效果图和代码
效果图:
也可以去笔者的网站上去看效果哦:http://ashuai.work:8888/#/myBack
代码:
<template> | |
<transition name="fade-transform"> | |
<div | |
v-show="visible" | |
class="backWrap" | |
:style="{ | |
bottom: bottom + 'px', | |
right: right + 'px', | |
}" | |
@click="goToTop" | |
> | |
<slot></slot> | |
</div> | |
</transition> | |
</template> | |
<script> | |
export default { | |
name: "myBack", | |
props: { | |
bottom: { | |
type: Number, | |
default: 72, | |
}, | |
right: { | |
type: Number, | |
default: 72, | |
}, | |
// 回到顶部出现的滚动高度位置 | |
showHeight: { | |
type: Number, | |
default: 240, | |
}, | |
// 拥有滚动条的那个 dom 元素的 id 或者 class,用于下方选中操作更改滚动条滚动距离 | |
scrollBarDom: String, | |
}, | |
data() { | |
return { | |
visible: false, | |
scrollDom: null, | |
}; | |
}, | |
mounted() { | |
if (document.querySelector(this.scrollBarDom)) { | |
this.scrollDom = document.querySelector(this.scrollBarDom); | |
// 不用给 window 绑定监听滚动事件,给对应滚动条元素绑定即可 | |
this.scrollDom.addEventListener("scroll", this.isShowGoToTop, true); | |
} | |
}, | |
beforeDestroy() { | |
// 最后要解除监听滚动事件 | |
this.scrollDom.removeEventListener("scroll", this.isShowGoToTop, true); | |
}, | |
methods: { | |
isShowGoToTop() { | |
// 获取滚动的元素,即有滚动条的那个元素 | |
if (this.scrollDom.scrollTop > 20) { | |
this.visible = true; | |
} else { | |
this.visible = false; | |
} | |
}, | |
goToTop() { | |
// 获取滚动的元素,即有滚动条的那个元素 | |
let scrollDom = document.querySelector(this.scrollBarDom); | |
// 获取垂直滚动的距离,看看滚动了多少了,然后不断地修改滚动距离直至为 0 | |
let scrollDistance = scrollDom.scrollTop; | |
/** | |
* window.requestAnimationFrame 兼容性已经可以了,正常都有的 | |
* */ | |
if (window.requestAnimationFrame) { | |
let fun = () => { | |
scrollDom.scrollTop = scrollDistance -= 36; | |
if (scrollDistance > 0) { | |
window.requestAnimationFrame(fun); // 只执行一次,想多次执行需要再调用 | |
} else { | |
scrollDom.scrollTop = 0; | |
} | |
}; | |
window.requestAnimationFrame(fun); | |
return; | |
} | |
/** | |
* 没有 requestAnimationFrame 的话,就用定时器去更改滚动条距离,使之滚动 | |
* */ | |
let timer2 = setInterval(() => { | |
scrollDom.scrollTop = scrollDistance -= 36; | |
if (scrollDistance <= 0) { | |
clearInterval(timer2); | |
scrollDom.scrollTop = 0; | |
} | |
}, 17); | |
}, | |
}, | |
}; | |
</script> | |
<style lang='less' scoped> | |
.backWrap { | |
position: fixed; | |
cursor: pointer; | |
width: 42px; | |
height: 42px; | |
background: #9cc2e5; | |
border-radius: 4px; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
transition: all 0.5s; | |
} | |
// 过渡效果 | |
.fade-transform-leave-active, | |
.fade-transform-enter-active { | |
transition: all 0.36s; | |
} | |
.fade-transform-enter { | |
opacity: 0; | |
transform: translateY(-5px); | |
} | |
.fade-transform-leave-to { | |
opacity: 0; | |
transform: translateY(5px); | |
} | |
</style> |
GitHub 仓库地址:https://github.com/shuirongshuifu/elementSrcCodeStudy
# 类比学习 reduce 循环解决了 forEach 循环可能需要一个初始变量的问题
我们类比一下学习,比如既然有了 forEach
循环,为啥还又新推出一个 reduce
循环呢?
原因: 某些场景下,reduce循环解决了forEach循环还需要再定义一个变量的问题。
似曾相识的感觉...
比如我们有一个需求,给一个数组求和。
forEach 写法
let arr = [1, 3, 5, 7, 9] | |
function forEachFn(params) { | |
let total = 0 | |
params.forEach((num) => { | |
total = total + num | |
}) | |
return total | |
} | |
let res1 = forEachFn(arr) | |
console.log(res1); |
reduce 写法
let arr = [1, 3, 5, 7, 9] | |
function reduceFn(params) { | |
return params.reduce((temp, num) => { | |
temp = temp + num | |
return temp | |
}, 0) | |
} | |
let res2 = reduceFn(arr) | |
console.log(res2); |
通过上述两段代码,我们可以看到, reduce
函数比 forEach
少写了一个 total
变量,千万不要小看这少写的东西,某些情况下,会节省很多的工作量呢!
window.requestAnimationFrame (callbackFn),但是却没有用到 cancelAnimationFrame。就是注意一下咱们的写法,cancelAnimationFrame 基本上很少用到(特殊情况可以用到)虽然平常大概率用不到,但是官方还是贴心的提供了清除 requestAnimationFrame 的方式。
window.requestAnimationFrame (callbackFn) 默认只会执行一次,就是说 cancelAnimationFrame 基本上用不到,代码中的继续执行是使用递归的形式,持续调用才会继续执行,当距离左侧超过 720 像素时,不去递归调用了,就停下来了。不像 setInterval 需要注意清除的时机
转自原文作者:水冗水孚原文链接