前言
在自己练习动画逻辑的时候,到线上看别人写的实例,发现大家都在用requestAnimationFrame方法来处理下一帧的渲染,我因此也产生疑问,requestAnimationFrame到底是什么?他的优势在哪?
一、API介绍
1、什么是requestAnimationFrame
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
简单来说,requestAnimationFrame 就是约定在下一次浏览器刷新前执行的一个定时器。类似setTimeout
2、语法
var ID = requestAnimationFrame(callback);
- 参数:callback是requestAnimationFrame的回调函数。该回调函数会被传入DOMHighResTimeStamp参数,该参数与performance.now()的返回值相同。浏览器下一次刷新时就会执行这个回调函数。
-
返回值:ID是requestAnimationFrame的调用时的返回值,是其在回调列表中的唯一的标识。你可以传这个值给
window.cancelAnimationFrame(ID)
以取消本次回调函数。
3、注意点
刚开始接触requestAnimationFrame时,大家经常有疑问:requestAnimationFrame什么时候执行?执行后时浏览器刷新都会执行回调函数吗?下边做简单回答(说多了,我就露馅了)
-
执行时机: 你调用一次requestAnimationFrame,从调用开始计时,下次浏览器刷新前,会执行回调函数。
需要注意的是 :调用一次,就只执行一次回调,若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用requestAnimationFrame(类似递归,下边会有介绍)
。 - 执行时间间隔:每次执行时间间隔会有差异,回调函数会被传入DOMHighResTimeStamp参数,DOMHighResTimeStamp指示当前被 requestAnimationFrame() 排序的回调函数被触发的时间。在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳,即使在计算上一个回调函数的工作负载期间已经消耗了一些时间。该时间戳是一个十进制数,单位毫秒,最小精度为1ms(1000μs)。
4、和setInterval、setTimeout的区别
上边提到requestAnimationFrame可以理解是一个定时器,大家就肯定想到了setInterval、setTimeout,他们有什么区别?
- 队列执行优先级不同:三者都是是异步 api,setTimeout 和 setInterval属于宏任务; requestAnimationFrame属于“渲染任务”(调用GUI 引擎),执行优先级在宏任务前,微任务之后。
//每个tick执行逻辑如下:
...->上一个宏任务 -> 微任务(下一个宏任务前的所有微任务) -> 渲染任务 -> 下一个宏任务 ->...
代码测试如下:
setTimeout(function(){
console.log("我是setTimeout", new Date().getTime())
},5)
requestAnimationFrame(function(){ //浏览器刷新频率是16.7ms左右,远大于5ms
console.log("我是requestAnimationFrame", new Date().getTime());
})
new Promise(function(resolve){
resolve('我是微任务')
}).then(res=>{
console.log(res, new Date().getTime());
})
//下边是打印信息:
//我是微任务 1662356658402
//我是requestAnimationFrame 1662356658403
//我是setTimeout 1662356658408
setTimeout写在requestAnimationFrame前,却在requestAnimationFrame后执行,而且通过打印时间戳可以看到相隔时间就是5ms左右,就是说明在执行requestAnimationFrame 后setTimeout才开始执行
典型的 MacroTask(宏任务) 包含了
setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering
等;
MicroTask(微任务)包含了process.nextTick, Promises, Object.observe, MutationObserver
等。
- 定时时间不同:setTimeout 和 setInterval的定时时间是我们自己定的,而requestAnimationFrame的定时时间是浏览器的刷新间隔时间(一般浏览器1s刷新60次,刷新间隔时间大约为16.7ms,所以不同浏览器刷新频率不同会导致定时时间不同),不用自己计算渲染时间,而且不会掉帧卡顿。
二、应用
1、基本应用
用法和setTimeout差不多,只是不用写定时时间。需要注意的是每调用一次requestAnimationFrame,只在下次浏览器刷新前执行一次回调函数,如果希望多次连贯执行回调,则需要在回调中再次通过requestAnimationFrame调用回调函数(递归)。
var animationId;//用来赋值requestAnimationFrame的id,为之后取消它做准备
function step(){
console.log('我就是下次浏览器刷新前需要执行的下一帧动画');
animationId = requestAnimationFrame(step);//为了在之后的每次浏览器刷新前都执行回调,递归调用回调
}
animationId = requestAnimationFrame(step);//最开始的调用
···
cancelAnimationFrame(animationId)//在满足某个条件时,取消上边requestAnimationFrame的调用,终止无休止的执行回调。
2、兼容处理
firefox、chrome、ie10以上, requestAnimationFrame 的支持很好,但不兼容 IE9及以下浏览器,所以需要在多个不同浏览器运行的话,就要做兼容性处理。
//简单的兼容性处理
window.requestAnimationFrame = (function() {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, 1000/60);
}
})();
上边这个简单的兼容处理还是存在问题的,因为并不是所有的设备的绘制时间间隔是1000/60ms,以及上面并没有cancel相关方法,所以,就有了下面这份更全面的兼容方法.
(function() {
var lastTime = 0;
var vendors = ['webkit', 'moz'];
//如果window.requestAnimationFrame为undefined先尝试浏览器前缀是否兼容
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] ||//webkit中此取消方法的名字变了
window[vendors[x] + 'CancelRequestAnimationFrame'];
}
//如果仍然不兼容,则使用setTimeOut进行兼容操作
if(!window.requestAnimationFrame) {
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16.7 - (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);
}
}
})();
上述的代码是由Opera浏览器的技术师Erik Möller设计的,使得更好得兼容各种浏览器,但基本上他的代码就是判断使用4ms还是16ms的延迟,来最佳匹配60fps。
三、总结和补充
1、requestAnimationFrame优点
requestAnimationFrame被专门用来处理动画,自然有他的优点存在的
- 动画流畅:动画每一帧的执行的间隔时间紧跟浏览器的刷新频率,动画更流畅,不会掉帧。
-
节能:
- 首先,在隐藏或不可见的元素中,requestAnimationFrame 将不会进行重绘或回流;
- requestAnimationFrame 是由浏览器专门为动画提供的 API,在运行时浏览器会自动优化方法的调用,如果浏览器在后台运行或者该页面tab在后台运行时,动画会自动暂停。
2、渲染引擎GUI
GUI 渲染引擎,用来处理浏览器的渲染操作,在 js 中渲染操作也是异步的。比如 操作DOM的代码会在事件队列中生成一个渲染任务,js 执行到这个任务时就会去调用 GUI 引擎渲染。
浏览器为了能够使得JS内部macro-task(宏任务)与DOM任务能够有序的执行(如果在 GUI 渲染的时候,js 改变了dom,那么就会造成渲染不同步
),会在一个macro-task执行结束后,在下一个macro-task执行开始前,调用 GUI 引擎渲染对页面进行重新渲染,并会阻塞 js引擎计算。而micro-task(微任务)不涉及DOM操作,在渲染任务前执行。
3、屏幕绘制频率相关补充
即图像在屏幕上更新的速度,也即屏幕上的图像每秒钟出现的次数,它的单位是赫兹(Hz)。 对于一般笔记本电脑,这个频率大概是60Hz, 可以在桌面上 右键 > 屏幕分辨率 > 高级设置 > 监视器 中查看和设置。这个值的设定受屏幕分辨率、屏幕尺寸和显卡的影响,原则上设置成让眼睛看着舒适的值都行。
市面上常见的显示器有两种,即 CRT和 LCD, CRT 是一种使用阴极射线管(Cathode Ray Tube)的显示器,LCD 就是我们常说的液晶显示器( Liquid Crystal Display)。
CRT 是一种使用阴极射线管的显示器,屏幕上的图形图像是由一个个因电子束击打而发光的荧光点组成,由于显像管内荧光粉受到电子束击打后发光的时间很短,所以电子束必须不断击打荧光粉使其持续发光。电子束每秒击打荧光粉的次数就是屏幕绘制频率。
而对于 LCD 来说,则不存在绘制频率的问题,因为 LCD 中每个像素都在持续不断地发光,直到不发光的电压改变并被送到控制器中,所以 LCD 不会有电子束击打荧光粉而引起的闪烁现象。
因此,当你对着电脑屏幕什么也不做的情况下,显示器也会以每秒60次的频率正在不断的更新屏幕上的图像。为什么你感觉不到这个变化? 那是因为人的眼睛有视觉停留效应,即前一副画面留在大脑的印象还没消失,紧接着后一副画面就跟上来了,这中间只间隔了16.7ms(1000/60≈16.7), 所以会让你误以为屏幕上的图像是静止不动的。而屏幕给你的这种感觉是对的,试想一下,如果刷新频率变成1次/秒,屏幕上的图像就会出现严重的闪烁,这样就很容易引起眼睛疲劳、酸痛和头晕目眩等症状。