requestAnimationFrame及用requestAnimationFrame实现setInterval
一、语法:
window.requestAnimationFrame()
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行,若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用 window.requestAnimationFrame(),如下:
function animate(timestamp) { console.log(timestamp) window.requestAnimationFrame(animate); } window.requestAnimationFrame(animate);
当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数 (即你的回调函数)。回调函数执行次数通常是每秒 60 次,但在大多数遵循 W3C 建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。为了提高性能和电池寿命,因此在大多数浏览器里,当requestAnimationFrame()
运行在后台标签页或者隐藏的<iframe>
里时,requestAnimationFrame()
会被暂停调用以提升性能和电池寿命。
回调函数会被传入DOMHighResTimeStamp
参数(animate方法的timestamp参数),DOMHighResTimeStamp
指示当前被 requestAnimationFrame()
排序的回调函数被触发的时间。在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳,即使在计算上一个回调函数的工作负载期间已经消耗了一些时间。该时间戳是一个十进制数,单位毫秒,最小精度为 1ms(1000μs)。
返回值
一个 long
整数,请求 ID ,是回调列表中唯一的标识。是个非零值,没别的意义。你可以传这个值给 window.cancelAnimationFrame()
以取消回调函数。
let animation = null; function animate(timestamp) { console.log(timestamp) if (animation) { window.cancelAnimationFrame(animation); } animation = window.requestAnimationFrame(animate); } animation = window.requestAnimationFrame(animate);
二、效果展示
浏览器中线程
定时器线程
浏览器定时计数器是一个单独的线程,触发定期行为后进入事件触发线程。
假设是在javascript线程,会因为阻塞导致计数不准确。
事件触发线程
当一个事件(鼠标点击、ajax请求、定时器等)触发后,会添加到队列中,等待计js线程处理。
异步http请求线程
浏览器有一个单独的线程用于处理ajax请求,当有回调时,进入事件触发线程
javascript线程
负责处理解析和执行javascript脚本程序
GUI 渲染线程
选择浏览器中html,以及对应的重绘。在javascript线程运行脚本期间,GUI是被暂时挂起的
Event Loop事件循环机制
在事件循环的过程中,异步事件返回的结果会被放在事件队列中。根据异步事件的类型,异步事件会被分为宏任务和微任务队列中。执行栈先去执行宏任务,当宏任务中内容为空,主线程会查看当前的微任务是否有事件存在,如果不存在会去执行下一次宏任务,如果存在,先执行当前的微任务。
一个简单的例子:
document.body.style = 'background:blue' console.log(1) new Promise(res=>{ document.body.style = 'background:gray' console.log(2) res(2) }).then((r)=>{ console.log(r) document.body.style = 'background:black' }) console.log(3) setTimeout(() => { console.log(4) document.body.style = 'background:pink' }, 10)
1.执行宏任务中的内容颜色变蓝
2.打印1
3.颜色变灰
4.打印2
5.打印3
6.执行微任务中的打印promise返回结果2
7.颜色变黑
8.GPU渲染
9.执行新的宏任务
10.打印4
11.颜色变粉
浏览器的样式为先黑色在变成粉色,会忽略掉第三个步骤,因为这个时候微任务还没有执行完,所以重绘的时候颜色会直接变成黑色
console.log(1) setTimeout(() => { console.log('setTimeout') }, 10); new Promise((res,rej)=>{ res(11) console.log("Promise") }).then(res=>{ console.log(res+"PromiseThen") }).catch(rej=>{ }) window.requestAnimationFrame(()=>{ console.log('requestAnimationFrame') }) console.log("hah") let aa = new Promise((res,rej)=>{ res(2) }) async function bb() { let a = await aa; try { console.log(a+"try") } catch (error) { console.log(a+"catch") } } bb() console.log("end")
1----Promise----hah----end----11PromiseThen----2try----requestAnimationFrame----setTimeout
这里的执行顺序:
1.宏任务开始 打印1 2.触发定时器线程、将回调放入事件队列中
3.promise打印promise,将promise的回调放入当前的微任务中
4.打印hah
5.继续执行promise,将回调放入微任务中
6.打印end
7.执行当前的微任务,打印
console.log(res+"PromiseThen")
打印11PromiseThen8.执行当前的微任务,打印
console.log(a+"try")
打印果2try9.执行requestAnimationFrame
console.log('requestAnimationFrame')
打印requestAnimationFrame10.GUP渲染
11.执行下次宏任务定时器中的
console.log('setTimeout')
打印setTimeout三、使用requestAnimationFrame实现setInterval
let startTime = new Date().getTime(); setTimeout(() => { let endTime = new Date().getTime(); console.log(endTime - startTime); }, 50) for (let i = 0; i < 20000; i++) { console.log(1); }
可以看到,设置了50毫秒后执行,实际执行延迟时间远大于这个数值,这就会导致动画效果并不会达到想要的效果。
动画是由浏览器按照一定的频率一帧一帧的绘制的,由css实现的动画的优势就是浏览器知道动画的开始及每一帧的循环间隔,能够在恰当的时间刷新UI,给用户一种流畅的体验,而setInterval或setTimeout实现的JavaScript动画就没有这么可靠了,因为浏览器压根就无法保证每一帧渲染的时间间隔,一般情况下,每秒平均刷新次数能够达到60帧,就能够给人流畅的体验,即每过 1000/60 毫秒渲染新一帧即可,但从上面的例子知,这一点单靠定时器是无法保证的。
而requestAnimationFrame的作用就是让浏览器流畅的执行动画效果。可以将其理解为专门用来实现动画效果的api,通过这个api,可以告诉浏览器某个JavaScript代码要执行动画,浏览器收到通知后,则会运行这些代码的时候进行优化,实现流畅的效果,而不再需要开发人员烦心刷新频率的问题了。
在需要指定间隔时间时就可以使用requestAnimationFrame来实现一个setInterval:
function customizeSetInterval(callback, interval) { let timer = null; let startTime = Date.now(); let loop = () => { let endTime = Date.now(); if (endTime - startTime >= interval) { startTime = endTime = Date.now(); callback(timer); } timer = window.requestAnimationFrame(loop); } loop(); return timer; } customizeSetInterval((timer) => { console.log(1); // cancelAnimationFrame(timer); }, 1000)