在制作页面动画时,我们可以通过定时器(setTimeoutsetInterval)循环操作DOM节点来实现动画的效果.

但是Google 最佳JS实践要求使用requestAnimationFrame代替定时器.这是为什么呢?

原因有三个:

  1. setTimeout\setInterval并不和JS在一个线程上,虽然你可以模拟浏览器刷新频率(1000/60),但如果JS线程执行时间较长,JS出超过这个时间去保存有setTimeout\setInterval回调的队列中拿回调执行.这个时候页面会出现卡顿的情况.
  2. 在一些浏览器中,运行有setTimeout\setInterval的脚本的页面,即使当前未被打开,定时器脚本依然是在后台运行的.这就浪费了计算机资源.
  3. 并不是所有的浏览器都是每秒60帧频率,所以setTimeout\setInterval的定时时间需要适配设备.

requestAnimationFrame就不会有上面这三个问题!

浏览器在每次重绘(repaint)之前,它都会先去执行requestAnimationFrame(callback)中的回调方法,所以它天生解决第1,3个问题.而当浏览器发现运行有requestAnimationFrame(callback)的页面在后台时,它会自动停止执行requestAnimationFrame(callback)中的回调,当它回到前台时又自动执行回调.

语法&兼容性

1
2
3
4
var requestId = window.requestAnimationFrame(callback);
// callback下一次重绘之前更新动画帧所调用的函数(即上面所说的回调函数)。该回调函数会被传入DOMHighResTimeStamp参数,该参数与performance.now()的返回值相同,它表示requestAnimationFrame() 开始去执行回调函数的时刻。

// requestId 一个 long 整数,请求 ID ,是回调列表中唯一的标识。是个非零值,没别的意义. 是给window.cancelAnimationFrame() 以取消回调函数

requestAnimationFrame只会执行一次,所以要实现连续的动画效果,需要在callback回调中调用它.

requestAnimationFrame也使用于CanvasWebGL中动画绘制.

目前大部分浏览器都是完美支持requestAnimationFrame的.

WechatIMG333.jpeg

假如还有不兼容的,则就可以使用下面的polyfill👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(function() {
var lastTime = 0;
var vendors = ['webkit', 'moz'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame =
window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
}

if (!window.requestAnimationFrame)
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};

if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}());

例子

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>
<title>Script-based animation using requestAnimationFrame</title>
<style>
div { position: absolute; left: 10px; padding: 50px;
background: crimson; color: white }
</style>
<script>
var requestId = 0;

function animate(time) {
document.getElementById("animated").style.left =
(time - animationStartTime) % 2000 / 4 + "px";
requestId = window.requestAnimationFrame(animate);
}
function start() {
animationStartTime = window.performance.now();
requestId = window.requestAnimationFrame(animate);
}
function stop() {
if (requestId)
window.cancelAnimationFrame(requestId);
requestId = 0;
}
</script>
<button onclick="start()">Click me to start!</button>
<button onclick="stop()">Click me to stop!</button>
<div id="animated">Hello there.</div>

上面👆代码运行效果

Future

requestAnimationFrame虽然好,但它是和JS运行在一个线程中的,如果它的回调耗时,会影响到整个JS执行效率的.

所以结合试验性apitransferControlToOffscreen,可以把动画操作放入 Web Worker中进行.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// index.js
const offscreenCanvas = document.getElementById("c").transferControlToOffscreen();
worker.postMessage(offscreenCanvas, [offscreenCanvas]);

// worker.js
let ctx, pos = 0;
function draw(dt) {
ctx.clearRect(0, 0, 100, 100);
ctx.fillRect(pos, 0, 10, 10);
pos += 10 * dt;
requestAnimationFrame(draw);
}

self.onmessage = function(ev) {
const transferredCanvas = ev.data;
ctx = transferredCanvas.getContext("2d");
draw();
};

参考

https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#animation-frames

https://www.w3.org/TR/animation-timing/#dom-windowanimationtiming-requestanimationframe

https://www.paulirish.com/2011/requestanimationframe-for-smart-animating/