需求:
假设在 60Hz
的屏幕下,我们想要实现一个 div
从左->右移动 100px
,并且 div
平滑移动,而不是一下到达终点。
实现思路:
60Hz
的屏幕,意味着每 16.7ms
屏幕就会更新图像,如果我们在屏幕每次刷新图像前,将 div
元素向右移动 1px
,这样的话,每次屏幕刷新的时候,我们的 div
元素都向右移动了一点点位置。但是由于间隔时间太短,我们的大脑和眼睛是无法处理的,所以我们会误认为 div
是平滑移动的。
所以我们的动画原理其实很简单,就是在很短的时间内不断更新相同间隔或者相同差别的图片,给我们人眼造成连续的假象。
我们常说的 60 帧电影,其实就是每秒 60 刷新 60 次。
我们知道了屏幕刷新率和动画的实现原理后,我们再来实现 div
从左向右移动这个动画就很简单了。
在 CSS3
动画出来以前,我们通常使用 JS
来实现动画效果,即使有了 CSS
动画,我们很多动画效果还是得依赖 JS
。而 JS
实现动画得两大利器便是 setTimeout
和 setInterval
,因为我们动画的原理就是不听的刷新图像,而定时器可以帮我们做这一操作。
比如我们使用 setInterval
实现 div
从左往右移动
代码如下:
<head>
<style>
.box {
width: 100px;
height: 100px;
background-color: blue;
position: relative;
}
</style>
</head>
<body>
<div class="box"></div>
</body>
<script>
let box = document.getElementsByClassName('box')[0];
// 动画函数
let num = 0;
function animation() {
if (num < 1000) {
num++;
box.style.left = num + 'px'
} else {
clearInterval(timer)
}
}
// 开启循环定时器
let timer = setInterval(animatio);
</script>
实现效果:
上段代码中我们编写了一个动画函数,代码很简单,就是让 div
元素向右移动 1px
。然后我们开启定时器不断重复该函数,需要注意的是,我们将函数执行的频率调为了差不多和屏幕刷新率一样,即 60Hz
的屏幕刷新率。
结合我们动画实现原理来看,相当于我们每次在屏幕刷新图像的时候 div
向右移动 1px
,由于人眼看不出区别,所以我们觉得 div
移动是平滑的。
前面是我们是依照屏幕刷新率来设定 setInterval
执行间隔时间的,理想情况下动画是平滑的,但是我们如果把间隔时间稍微改一改,会发生什么效果呢?
修改代码如下:
let timer = setInterval(animation, 500);
实现效果:
从上图可以看出,我们将函数执行时间间隔调为了 500ms
,意味着每 500ms
div
元素向前移动 1px
,我们发现动画出现了明显的卡顿,这是为什么呢?
这是因为 500ms
的间隔我们人眼是能够感知的,而屏幕的刷新率我们是感知不到的。就相当于 500ms
的时候,div
向前移动了 1px
,然后停住了,我们是感知到了,所以下次移动的时候,我们能够明显感知到卡顿。
很多小伙伴可能会问,我为什么要设置为 500ms
,设置为与刷新率一致不就好了么?我们继续往下看。
上一节中我们手动将间隔时间改为了 500ms
,虽然可能有些极端,但是在实际项目中,setInterval
本身就是有一些问题的。
影响动画的问题:
setInterval
是异步的,也就是意味着 JS
代码执行的时候会将它放入异步队列中,所以它的执行时间并不确定。结合我们上述动画,假设我们设置的是 16.7ms
执行一次,但是在实际项目中,有可能有很多异步函数,这个时候 setInterval
执行时间隔就可能不是准确的 16.7ms
,这就会造成卡顿。30Hz
、60Hz
、90Hz
、120Hz
等等,以后还会更多,但是我们传入 setInterval
的时间间隔是固定的,这就有可能造成在不同设备上的动画流畅度不同。setInterval
会一直在后台执行,即使我们访问其它页面时。我们做个假设,让大家更好理解问题所在:
同样是 div
从左往右移动的动画前提下,我们使用 setInterval
实现动画,且间隔设置为 10ms
,屏幕的绘制过程大致如下:
依次执行…
上面的流程中,总共费时33.4ms
,屏幕刷新了两次。正常情况下,第一次屏幕刷新时,left=1px
,第二次屏幕刷新时,left
应为 2px
,但是我们发现第二次屏幕刷新时,left
直接变为了 3px
,这是由于我们的 setInterval
间隔设置为 10ms 的原因。这就造成了屏幕上的 div
的 left
直接从 1px
变为了 3px
,这就会造成跳帧,即卡顿。
前面我们介绍的 setInterval
是我们使用 JS
实现动画常用的手段,不可否认,在以前确实可以这样使用,也是最好的办法。
但是我们也需要正面这些问题,特别是随着电子设备的不断更新换代,屏幕的刷新率也出现很多种的情况下,如果继续使用定时器来实现动画,很可能会造成动画的卡顿以及性能的消耗。
有问题就有解决方案!requestAnimationFrame API
就非常完美的解决了定时器实现动画的各种问题。
我们先来看看官网如何解释它的。
官网解释:
当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数 (即你的回调函数)。
通过官网的解释我们似乎还不能很明了的知道它为什么能解决哪些问题,别急,我们一步一步来看。我们首先需要明确的是 requestAnimationFrame
是一个 API,即一个原生方法。
我们先来看看 requestAnimationFrame
如何使用,再来感受它的魅力!
它的使用非常简单,因为它是一个原生 API
。
语法如下:
window.requestAnimationFrame(callback);
参数解释:
callback
:一个回调函数,也就是我们的动画函数,就好比我们定时器接收的回调函数一般,它是下一次重绘之前更新动画帧所调用的函数 。callback
参数我们需要重点关注它的执行时间,上面的重绘可以简单理解为重新绘制页面,动画帧也就是更新图像,也就是我们前面所说的屏幕刷新率:60Hz 的屏幕下大概每 16.7ms 就会更新依次图像,这就是动画帧。可以简单理解为回调函数会在屏幕刷新的时候调用。
返回值:
一个 long
整数,请求 ID
,是回调列表中唯一的标识。是个非零值,没别的意义。你可以传这个值给 window.cancelAnimationFrame()
以取消回调函数。
接下来我们来总结一下 requestAnimationFrame
的特点,知道了它的特点我们就明白它为什么厉害了!
requestAnimationFrame
方法只接收一个参数,即回调函数,意味着我们不用间隔。因为requestAnimationFrame
中的回调函数执行频率通常为 60 次每秒,但是它会随着屏幕刷新率的变化而变化。总之,回调函数的执行频率与浏览器的刷新次数相匹配。requestAnimationFrame
不会在后台一直执行,它会在页面出现的时候才会执行,比如 document.hide
为 true
的时候,而我们的定时器是一直会执行的。可以节省 CPU
和 GPU
等等。我们将前面使用 setInterval
实现的动画再用 requestAnimationFrame
实现一遍。
代码如下:
let box = document.getElementsByClassName('box')[0];
// 动画函数
let num = 0;
function animation() {
if (num < 1000) {
num++;
box.style.left = num + 'px'
// 请求动画帧,即屏幕刷新的时候执行回调函数
requestAnimationFrame(animation); // 继续执行该函数
} else {
cancelAnimationFrame(); // 清楚该函数
}
}
let raID = requestAnimationFrame(animation);
实现效果:
上段代码很简单,我们将动画函数传入的 requestAnimationFrame
方法中,然后在动画函数中继续调用 requestAnimationFrame
方法,以保证动画持续进行,因为我们改变了 left
,是需要更新动画的。而且从上图可以看出,当我们切换到其它页面时,动画停留在了那儿,切换回来时,动画继续执行。
对于这种比较新的东西,我们最好还是看看它的兼容性如何,如下图所示:
我们看到它的兼容性很不错,我们可以放心的使用。
本篇文章我们用了大量篇幅来讲解动画原理,其实理由很简单,只要你搞懂了动画原理,你就明白了我们为什么要使用 requestAnimationFrame
方法,其中的道理你就自然而然的明白了!
最后,可能有些小伙伴会疑惑:为什么不用 CSS
实现上述动画呢?
确实,本篇文章的动画可以利用 CSS
实现,但是如果项目遇到无法使用 CSS
实现的动画呢,该怎么办呢?
因篇幅问题不能全部显示,请点此查看更多更全内容