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事件循环机制

在事件循环的过程中,异步事件返回的结果会被放在事件队列中。根据异步事件的类型,异步事件会被分为宏任务和微任务队列中。执行栈先去执行宏任务,当宏任务中内容为空,主线程会查看当前的微任务是否有事件存在,如果不存在会去执行下一次宏任务,如果存在,先执行当前的微任务。

 

 

浏览器中的宏任务主要包含定时器、io操作,js操作,微任务主要是promise、async,requestAnimationFrame姑且作为一种特殊的任务,在执行完微任务后,在下次重绘之前调用指定的回调函数更新动画。
一个简单的例子:
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、3、2、4。
1.执行宏任务中的内容颜色变蓝
2.打印1
3.颜色变灰
4.打印2
5.打印3
6.执行微任务中的打印promise返回结果2
7.颜色变黑
8.GPU渲染
9.执行新的宏任务
10.打印4
11.颜色变粉
浏览器的样式为先黑色在变成粉色,会忽略掉第三个步骤,因为这个时候微任务还没有执行完,所以重绘的时候颜色会直接变成黑色
例2:
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")打印11PromiseThen
8.执行当前的微任务,打印console.log(a+"try")打印果2try
9.执行requestAnimationFrameconsole.log('requestAnimationFrame')打印requestAnimationFrame
10.GUP渲染
11.执行下次宏任务定时器中的console.log('setTimeout')打印setTimeout

三、使用requestAnimationFrame实现setInterval

随着前端的发展,css已经能够实现非常多的动画特效,但是仍然存在css无法完成的动画任务(比如页面滚动),requestAnimationFrame之前通常的解决方案都是使用js中的setInterval来设置定时器来实现动画特效,然而,使用计时器真的可靠吗?由于JavaScript是单线程的,所以定时器的实现是在当前任务队列完成后再执行定时器的回调的,假如当前队列任务执行时间大于定时器设置的延迟时间,那么定时器就不是那么可靠了,如下所示:
        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)

 

posted @ 2022-09-15 16:42  vickylinj  阅读(912)  评论(0编辑  收藏  举报