嗯哼?没听过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>
|
效果:

当然,在理想的情况下,动画运行的很流畅,但是我们别忘了,在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
| 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加载缓慢请稍等):

我们打开chorme的FPS检测可以看到,尽管我们没有设置刷新间隔,requestAnimationFrame实现的动画自动会处于60fps,这就是requestAnimationFrame自己给我们设置的,这个值会随着不同显示器来改变,即144hz的显示器就是144fps,这就不会造成不同显示器导致动画丢帧现象。
优势
除此之外,requestAnimationFrame还有以下两个优势:
CPU优化:使用setTimeout时,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,这时候刷新动画是没有意义的。而requestAnimationFrame则全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此按照浏览器内核来的requestAnimationFrame也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了CPU开销。
函数节流:在高频率事件(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) || !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 :)