嗯哼?没听过requestAnimationFrame?进来看看呗~

前言

有次在《CSS权威指南》中查animation的属性时,发现文章末尾还提到一个HTML的API叫requestAnimationFrame也能实现动画,好奇宝宝感觉谷歌一下,发现这还真的是一个先进的动画实现方案呀,下面听我一一述说。

为什么不能用setInterval与setTimeout来实现

开始讲requestAnimationFrame前,如果让我用js来实现一个动画,我第一时间想到的是JavaScript定时器来实现,即setInterval与setTimeout。

我们给setInterval定一个特定的间隔,每个间隔都改变元素的样式,这样就能形成一个简单的动画,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<div id="myDiv" style="background-color: pink;width: 0;height: 30px;line-height: 30px;">0%</div>
<button id="btn">run</button>
</body>
<script>
var timer;
var btn = document.getElementById('btn')
btn.onclick = function () {
clearInterval(timer);
myDiv.style.width = '0';
timer = setInterval(function () {
if (parseInt(myDiv.style.width) < 100) {
myDiv.style.width = parseInt(myDiv.style.width) + 1 + 'px';
myDiv.innerHTML = parseInt(myDiv.style.width) / 1 + '%';
} else {
clearInterval(timer);
}
}, 16);
}
</script>
</html>

一样的道理,我们用setTimeout也是差不多的,只是在函数中定义一个递归:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<div id="myDiv" style="background-color: pink;width: 0;height: 30px;line-height: 30px;">0%</div>
<button id="btn">run</button>
</body>
<script>
var timer;
var btn = document.getElementById('btn')
btn.onclick = function () {
clearTimeout(timer);
myDiv.style.width = '0';
timer = setTimeout(function fn() {
if (parseInt(myDiv.style.width) < 100) {
myDiv.style.width = parseInt(myDiv.style.width) + 1 + 'px';
myDiv.innerHTML = parseInt(myDiv.style.width) / 1 + '%';
timer = setTimeout(fn, 16);
} else {
clearTimeout(timer);
}
}, 16);
}
</script>
</html>

效果:

image

当然,在理想的情况下,动画运行的很流畅,但是我们别忘了,在Javascript中, setInterval和setTimeout 任务被放进了异步队列中,只有当主线程上的任务执行完以后,才会去检查该队列里的任务是否需要开始执行,因此 setTimeout 的实际执行时间一般要比其设定的时间晚一些,所以,当页面上的js同异代码运行很复杂时,对动画定时器造成的影响是非常不可预测的。

还有一个问题就是,动画间隔设置时间是固定的,这个上面代码我们设置的间隔时间是166ms,这个不是随意设置的,这是根据目前大多数电脑显示器的刷新率是每秒60帧,也就是(60/1000ms = 16ms),但是其他的一些设备可能高于或者低于这个刷新率,这就会影响显示器的正常动画渲染,导致丢帧现象,这种现象就会引起动画卡顿。

那么,既然定时器无法做到,还有别的选择吗?

动画利器:requestAnimationFrame

requestAnimationFrame采用系统时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果,也就是说浏览器的内核帮我们解决了上面遇到的所有问题,我们先来了解他的用法。

用法

requestAnimationFrame的用法与settimeout很相似,只是不需要设置时间间隔而已(浏览器内核已经设置好最佳时间)。requestAnimationFrame使用一个回调函数(一般在这个函数写动画变换规律和结束动画条件)作为参数,这个回调函数会在浏览器重绘之前调用。它自动返回一个整数,表示定时器的编号,这个值可以传递给cancelAnimationFrame用于取消这个函数的执行。

1
2
3
4
var timer = requestAnimationFrame(function () {
// 动画变换规律和结束动画条件
});
console.log(timer); // 1 这个数字是根据页面中是否有其他的定时器来确定的,定时器的编号不会相同

用下面函数来停止动画:

1
cancelAnimationFrame(timer);

实例

我们实现上面用setTimeout的效果,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<div id="myDiv" style="background-color: pink;width: 0;height: 30px;line-height: 30px;">0%</div>
<button id="btn">run</button>
</body>
<script>
var timer;
var btn = document.getElementById('btn')

btn.onclick = function () {
myDiv.style.width = '0';
cancelAnimationFrame(timer);
timer = requestAnimationFrame(function fn() {
if (parseInt(myDiv.style.width) < 100) {
myDiv.style.width = parseInt(myDiv.style.width) + 1 + 'px';
myDiv.innerHTML = parseInt(myDiv.style.width) / 1 + '%';
timer = requestAnimationFrame(fn); // 继续执行动画
} else {
cancelAnimationFrame(timer); // 截止动画
}
});
}
</script>
</html>

效果如下(gif加载缓慢请稍等):

image

我们打开chorme的FPS检测可以看到,尽管我们没有设置刷新间隔,requestAnimationFrame实现的动画自动会处于60fps,这就是requestAnimationFrame自己给我们设置的,这个值会随着不同显示器来改变,即144hz的显示器就是144fps,这就不会造成不同显示器导致动画丢帧现象。

优势

除此之外,requestAnimationFrame还有以下两个优势:

  1. CPU优化:使用setTimeout时,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,这时候刷新动画是没有意义的。而requestAnimationFrame则全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此按照浏览器内核来的requestAnimationFrame也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了CPU开销。

  2. 函数节流:在高频率事件(resize,scroll等)中,为了防止在一个刷新间隔内发生多次函数执行,使用requestAnimationFrame可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销。

兼容

因为还要考虑到万恶的IE浏览器,IE9-浏览器不支持该方法,可以使用setTimeout来兼容,这段代码是在在github得到普遍认可的,如果在需要兼容低版本浏览器时,可以用到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if (!Date.now)
Date.now = function() { return new Date().getTime(); };

(function() {
'use strict';

var vendors = ['webkit', 'moz'];
for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
var vp = vendors[i];
window.requestAnimationFrame = window[vp+'RequestAnimationFrame'];
window.cancelAnimationFrame = (window[vp+'CancelAnimationFrame']
|| window[vp+'CancelRequestAnimationFrame']);
}
if (/iP(ad|hone|od).*OS 6/.test(window.navigator.userAgent) // iOS6 is buggy
|| !window.requestAnimationFrame || !window.cancelAnimationFrame) {
var lastTime = 0;
window.requestAnimationFrame = function(callback) {
var now = Date.now();
var nextTime = Math.max(lastTime + 16, now);
return setTimeout(function() { callback(lastTime = nextTime); },
nextTime - now);
};
window.cancelAnimationFrame = clearTimeout;
}
}());

总结

这次探究问题让我深深领悟到对于新的知识要保持灵敏的嗅探性,因为只有更好的东西才会被传播出来,所以时间不停,学无止境。requestAnimationFrame是一个非常值得普遍使用的功能,因为我们用js来控制CSS的animation并不是那么简单,有了requestAnimationFrame我们就能不需要担心性能的条件下,大胆地进行动画开发。

Time tames the strongest grief :)