Description
一、背景(让JS动画更丝滑)
The number of callbacks is usually 60 times per second
refresh rate
执行时机
repaint
1.1 流畅动画的标准
理论上说,FPS 越高,动画会越流畅,目前大多数设备的屏幕刷新率为 60 次/秒,所以通常来讲 FPS 为 60 frame/s 时动画效果最好,也就是每帧的消耗时间为 16.67ms。
1.2 代替setTimeout
/setInterval
实现JS动画
尽管setTimeout
/setInterval
这两个API可以实现JS动画,但是丢帧的风险很高。因为回调函数会在帧中的某个时间点执行,也许是在最后面,会导致单个或多帧丢失。
二、语法
2.1 基础
id = requestAnimationFrame(callback)
1. callback
只是个回调函数,不是事件处理函数。其实参是个DOMHighResTimeStamp,不是事件对象;
requestAnimationFrameId = requestAnimationFrame((timestamp) => {
console.log(`rAF callback ${timestamp}, performance.now=${performance.now()}`)
})
这里的Demo中timestamp
要小于performance.now()
的返回值。因为前者赋值时发生的早,于callback
执行。
2. 调用频率
回调函数执行次数通常是每秒60次(递归调用情况下),但在大多数遵循W3C建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。即刷新频率越高,执行次数越多。
注意:requestAnimationFrame
并不像setInterval
那样一次调用一直调用,requestAnimationFrame
被调用一次,则callback
执行一次(callback
更多的是叫tick
)。
3. 批调用
多次调用requestAnimationFrame
注册的多个callback
会在下次repaint
前统一调用:
- 所有
callback
的实参是一样的(即触发时间点一致); - 执行顺序是注册顺序。
2.2 什么时候执行?【TODO 了解浏览器的渲染机制】
浏览器执行下次repaint前。
2.3 callback
耗时多久最好?
跟其他函数一样, callback
如果耗时比较久的话会影响浏览器渲染的,会出现卡顿(浏览器FPS会下降)。为了不影响浏览器渲染还行时间应该小于16.7ms。
2.4 监测应用卡顿
利用requestAnimationFrame(callback)
每秒60次调用频率计算浏览器的FPS。
var lastCallTime;
var count= 0;
function loop(timestamp) {
if(timestamp == null ) {
lastCallTime = performance.now();
} else {
var timeSpan = timestamp - lastCallTime;
++count;
if(timeSpan > 1000) {
console.log(`count: ${count}`);
lastCallTime = timestamp;
count = 0;
}
}
requestAnimationFrame(loop)
}
- 计算1s内
callback
调用次数。
2.5 优点
- 批调用
The browser can optimize concurrent animations together into a single reflow and repaint cycle, leading to higher fidelity animation.
即多次调用requestAnimationFrame
注册的回调函数,在下次repain
前同步一次调用(实现了批调用)
2. 切到后台tab或者隐藏iframe时,requestAnimationFrame
回调函数会停止触发。减少资源(CPU,GPU,内存等)占用。
3. 更省电(因为减少资源使用)
2.6 requestAnimationFrame
== throttle(func, 16)
?
requestAnimationFrame
有点类似对JS动画处理函数的节流操作。
三、polyfill
function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
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的实参不是实时利用`new Date().getTime()`获取的,而是个理想的预估时间
callback(currTime + timeToCall);
}, timeToCall);
// lastTime为啥不是在setTimeout回调函数里更新,而是在这里同步更新
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}());
- 各种厂商前缀,利用
for
循环不错,省得对requestAnimationFrame
和cancelAnimationFrame
分别进行||
判断; webkitCancelRequestAnimationFrame
是个特例, Chrome 16 haswebkitCancelRequestAnimationFrame
;- 利用
setTimeout
做兜底
- 逻辑就像是个
throttle(callback, 16)
。setTimeout
的delay时间是动态计算的,不是写死的16ms
,这样对于执行间隔大于16ms的会立马执行,而小于16ms的则动态计算还需要等待的时间。
- 上面polyfill有个问题
- FireFox有些版本只存在
raf
,但是不存在caf
。所以应该只要两者任意不存在,就要启动setTimeout 兜底。 callback
的参数计算利用的Date.now
时间戳,不符合API规则。
应该使用performance.now()
- 每次调用都会产生一个新的
setTimeout
(假如走到兜底逻辑了),并没有实现原始API聚合多个处理函数逻辑的功能。
以Tino Zijdel:paulirish/rAF.js为基础的npm库raf实现的polyfill更符合好些(解决了上诉polyfill的不足)。
四、总结:
作为代替setTimeout
/setInterval
实现JS动画的解决方案
- 规定好调用时间点,不掉帧。并且进行的是批调用,有利于性能优化。
- 切到后台的tab或者隐藏的iframe会暂停
callback
调用,节省资源。