定时器、计时器与requestAnimationFrame、requestIdleCallback

1,242 阅读8分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。详情

1、setTimeout

setTimeout的运行机制:执行该语句时,是立即把当前定时器代码推入事件队列,当定时器在事件列表中满足设置的时间值时将传入的函数加入任务队列,之后的执行就交给任务队列负责。但是如果此时任务队列不为空,则需等待。如果任务队列为空,则进入执行栈执行。所以执行定时器内代码的时间可能会大于设置的时间

定义setTimeout执行函数 -> 事件队列(工作线程)挂起 -> 任务队列 -> 执行栈 -> 执行完毕销毁

setTimeout(() => {
    console.log(1);
}, 0)
console.log(2);

输出结果是 2 1

  1. 在上述代码中,console.log(2)是直接进入执行栈执行。虽然setTimeout的延时是0毫秒,但是要经历任务挂起到队列,然后再到任务队列,然后等执行栈中的console.log(2)执行完毕清空执行栈后才会执行。执行过程图解

  2. 实际上,上面的代码并不是立即执行的,这是因为setTimeout有一个最小执行时间,HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔)不得低于4毫秒。 当指定的时间低于该时间时,浏览器会用最小允许的时间作为setTimeout的时间间隔,也就是说即使我们把setTimeout的延迟时间设置为0,实际上可能为 4毫秒后才事件推入任务队列

  3. 定时器内代码的时间可能会大于设置的时间:例如setTimeout200ms执行一个函数,但是只是200ms后会将这个函数放入任务队列,队列前端可能还有其他任务。等排到当前的setTimeout任务后还需要等执行栈清空后才执行。所以执行的时机基本上都会大于200ms了。

image-20211229181842275.png

例如这样一个过程:

btn.onclick = function(){
    setTimeout(function(){
        console.log(1);
    },250);
    // ...其他操作代码
}

击该按钮后,

①首先将onclick事件处理程序加入队列。该程序执行后才设置定时器。

250ms后,指定的代码才被添加到队列中等待执行。

③如果上面代码中的onclick事件处理程序执行了300ms,那么定时器的代码至少要在定时器设置之后的300ms后才会被执行。

④队列中所有的代码都要等到javascript进程空闲之后才能执行,而不管它们是如何添加到队列中的。

image-20220226163712198.png

如图所示,①在5ms时创建了定时器放入到工作线程挂起并开始计时。

②尽管在255ms时定时器到时间,定时器代码添加到了任务队列等待入栈执行,但这时候还不能执行

③因为onclick事件处理的其他操作程序仍在运行(执行栈被占用)。

④定时器代码最早能执行的时机是在300ms处。onclick事件执行完毕后执行栈清空,此时定时器的代码才真正被执行,即onclick事件处理程序结束之后。

2、setInterval

周期性的调用函数。setInterval则是每次都精确的隔一段时间推入一个事件(但是,事件的执行时间不一定就不准确,还有可能是这个事件还没执行完毕,下一个事件就来了)

setInterval存在的一些问题:

JavaScript中使用 setInterval 开启轮询。定时器代码可能在代码再次被添加到队列之前还没有完成执行,结果导致定时器代码连续运行好几次,而之间没有任何停顿。而javascript引擎对这个问题的解决是:当使用setInterval()时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。这确保了定时器代码加入到队列中的最小时间间隔为指定间隔。

但是,这样会导致两个问题:

  • 某些间隔被跳过;
  • 多个定时器的代码执行之间的间隔可能比预期的小

假设,某个onclick事件处理程序使用setInterval()设置了200ms间隔的定时器。如果事件处理程序花了300ms多一点时间完成,同时定时器代码也花了差不多的时间,就会同时出现跳过某间隔的情况

image-20220226165003031.png

例子中的第一个定时器是在205ms处添加到队列中的,但是直到过了300ms处才能执行。当执行这个定时器代码时,在405ms处又给队列添加了另一个副本。在下一个间隔,即605ms处,第一个定时器代码仍在运行,同时在队列中已经有了一个定时器代码的实例。结果是,在这个时间点上的定时器代码不会被添加到队列中。

使用setTimeout构造轮询能保证每次轮询的间隔。

setTimeout(function fn(){
    console.log('我被调用了');
    setTimeout(fn, 100);
},100);

这个模式链式调用了setTimeout(),每次函数执行的时候都会创建一个新的定时器。第二个setTimeout()调用当前执行的函数,并为其设置另外一个定时器。

这样做的好处是,在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。而且,它可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。

3、requestAnimationFrame

60fps与设备刷新率

目前大多数设备的屏幕刷新率为60次/秒。页面中的动画 或 滚动的页面 的渲染页面效果的每一帧的速率要跟设备屏幕的刷新频率一致。

卡顿:每个帧的预算时间约为16.6毫秒。但实际上,浏览器有整理工作要做,因此所有工作是需要在10毫秒内完成。如果无法符合此预算,帧率将下降,并且内容会在屏幕上抖动。此现象通常称为卡顿,会对用户体验产生负面影响。

跳帧: 假如动画切换在 16ms, 32ms, 48ms时分别切换,跳帧就是假如到了32ms,其他任务还未执行完成,没有去执行动画切帧,等到开始进行动画的切帧,已经到了该执行48ms的切帧。就好比你玩游戏的时候卡了,过了一会,你再看画面,它不会停留你卡的地方,或者这时你的角色已经挂掉了。必须在下一帧开始之前就已经绘制完毕下

requestAnimationFrame实现动画

requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。

  1. requestAnimationFrame 之前,主要借助 setTimeout/ setInterval 来编写 JS 动画,而动画的关键在于动画帧之间的时间间隔设置,这个时间间隔的设置有讲究,一方面要足够小,这样动画帧之间才有连贯性,动画效果才显得平滑流畅;另一方面要足够大,确保浏览器有足够的时间及时完成渲染。

  2. 显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。

  3. 使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。

  4. requestAnimationFrame 是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。

  5. 用法:requestAnimationFrame 使用一个回调函数作为参数。这个回调函数会在浏览器重绘之前调用。

    requestID = window.requestAnimationFrame(callback); 
    ​
    // 按照1秒钟60次(大约每16.7毫秒一次),来模拟requestAnimationFrame。
    window.requestAnimFrame = (function(){
        return  window.requestAnimationFrame       || 
                window.webkitRequestAnimationFrame || 
                window.mozRequestAnimationFrame    || 
                window.oRequestAnimationFrame      || 
                window.msRequestAnimationFrame     || 
                function( callback ){
                window.setTimeout(callback, 1000 / 60);
            };
    })();
    

4、requestIdleCallback()

requestIdleCallback()方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。

requestAnimationFrame会在每次屏幕刷新的时候被调用,而requestIdleCallback则会在每次屏幕刷新时,判断当前帧是否还有多余的时间,如果有,则会调用requestAnimationFrame的回调函数,

image-20220226172954630.png

如上图,是两个连续执行帧的时间(两个16.6ms)。黄色的为空闲时间。requestIdleCallback 中的回调函数仅会在每次屏幕刷新并且有空闲时间时才会被调用。

利用这个特性,我们可以在动画执行的期间,利用每帧的空闲时间来进行数据发送的操作,或者一些优先级比较低的操作,此时不会使影响到动画的性能,或者和requestAnimationFrame搭配,可以实现一些页面性能方面的的优化,

react 的 fiber 架构也是基于 requestIdleCallback 实现的, 并且在不支持的浏览器中提供了 polyfill

总结

  • js单线程模型和任务队列出发理解 setTimeout(fn, 0),并不是立即执行。
  • JS 动画, 用requestAnimationFrame 会比 setInterval 效果更好
  • requestIdleCallback()常用来切割长任务,利用空闲时间执行,避免主线程长时间阻塞