setTimeout, setlnterval和新生代的 requestAnimation, requestldleCallback🫡

setTimeout, setlnterval和新生代的 requestAnimation, requestldleCallback🫡

传统定时器的误差出现的原因

  • setInterval、setTimeout 实现都会出现误差,这源于js的单线程。
  • 他们的回调函数并不是到时候立即执行,而是等系统计算资源空闲下来后才会执行。
  • setInterval、setTimeout 都属于宏任务(结合事件循环机制)。

setInterval的误差

setInterval 定时器在一定时间内执行某个方法,下面通过一个案例的思想💭向大家演示它在使用中出现误差的过程。

  1. 有一个setTimeout,4s后触发一次,耗时9s
  2. 有一个setlnterval,每5s触发一次,每次耗时4S
  3. 用户在程序第3s的时候,触发点击事件,添加onClick事件,onClick耗时6s

按照以上逻辑直接来看浏览器是如何执行的:
setTimeout, setlnterval和新生代的 requestAnimation, requestldleCallback🫡
看到控制台的最后一行输出,此时是setInterval的第二次执行,按理来说它应该与上一次间隔5s后执行,为什么这里只间隔了4s呢🤔?

接着往下看:
setTimeout, setlnterval和新生代的 requestAnimation, requestldleCallback🫡

📍为什么,定时器代码间隔会比预期要小?

setTimeout, setlnterval和新生代的 requestAnimation, requestldleCallback🫡

在讲解以上时序图之前,必须先掌握三点概念:

  • JS主线程和计时器线程是相互独立的;
  • 宏任务队列一次只能执行一个 ;
  • setInterval往宏任务队列注册事件时,如果它上一个回调函数没有被执行,那么新的事件不会被注册;

现在开始讲解上面的时序图:

  1. 在刚开始第3s时,执行了点击事件,onclick事件进入宏任务队列,并被执行。但是在第4s时setTimeout回调进入宏任务队列,setInterval在第5s注入回调。
  2. 在第9s时执行setTimeout回调,当timeout事件执行完毕后时间已经来到了第18s,此时执行第一个setInterval。在这个过程中的第10s和第15s时setInterval试图继续注入回调Interval-2和Interval-3,但由于Interval-1还没被执行,所以注入失败,Interval-2和Interval-3被丢弃。
  3. 在第20s时,Interval-4成功注入宏任务队列,第22s时,Interval-4开始执行。22s和18s之间间隔4s。
  4. 第30秒注册Interval-6,随后立即执行,没有收到其他延时干扰,所以在Interval-7注册时,setInterval时间间隔恢复正常。

setTimeout, setlnterval和新生代的 requestAnimation, requestldleCallback🫡

setlnterval-累计效应

  • 定时器代码执行之间的间隔可能比你预期的要小
  • 定时器某些间隔被跳过(例如Interval-2和Interval-3的无效注册)

setInterval & setTimeout的执行时机

  1. 代码层面chrome 中 setInterval 最小延迟是1ms,而 setTimeout 则是0ms

  2. 以chrome浏览器为例, setTimeout 中 delay 小于1ms时和预期行为不符,是因为源码中小于1ms被定义为与0ms一样的‘立即’执行任务了。还有个小点 setTimeout 的delay是向下取整的即1.9ms和1ms等价、0.8ms和0ms等价

  3. setTimeout 的最大延迟时间是2的31次方-1,超过这个数字会立即输出

  4. node中 setTimeout 的delay小于1ms时会被修改为1ms

详细分析过程看这里

简单的示例一:

// 设置最大值
setTimeout(()=>{
    console.log('a')
}, 2**31);
//设置最小值
setTimeout(()=>{
    console.log('b')
}, 1);
setTimeout(()=>{
    console.log('c')
}, 0.5);
//设置0
setTimeout(()=>{
    console.log('d')
}, 0);

输出结果:a c d b

简单的示例二:

// 设置最大值
setInterval(()=>{
    console.log('a')
}, 2**31);
//设置最小值
setInterval(()=>{
    console.log('b')
}, 1);
setInterval(()=>{
    console.log('c')
}, 0.5);
//设置0
setInterval(()=>{
    console.log('d')
}, 0);

输出结果:a b c d

setTimeout 5 层以上的嵌套会导致至少 4ms 的延五成

let t1 = performance.now();
//打印时间
function printTime(count) {
const now = performance. now();
console.log(count,"==时间差:",now -t1);
t1 = now;
}

setTimeout(()=>{
  printTime(1);
  setTimeout(()=>{
    printTime(2);
    setTimeout(()=>{
      printTime(3);
      setTimeout(()=>{
        printTime(4);
        setTimeout(()=>{
          printTime(5);
          setTimeout(()=>{
            printTime(6);
          },0)
        },0)
      },0)
    },0)
  },0)
},0)

输出结果:

setTimeout, setlnterval和新生代的 requestAnimation, requestldleCallback🫡

拓展:为什么定时器有最小时延

📍setTimeout 与 setlnvertal 的区别

  • setTimeout 是递归循环,它基本上可以保证代码的执行顺序,每次的至少延迟时长大于等于设置的时间。
  • setlnvertal 每次定时触发执行回调函数,它的执行时间间隔可能会比期待的要小,而且不关心前一个回调函数是否执行,不会重复注册回调。

新生代定时器

requestAnimationFrame

  • requestAnimationFrame 告诉浏览器,我希望执行一个动画,并要求浏览器在下次重绘之前执行指定的回调函数更新动画

  • 回调函数执行次数与浏览器屏幕的刷新次数匹配。一般为每秒60次

requestAnimationFrame 在事件循环中的执行时机

  • 回顾事件循环步骤:1个宏任务 -> 所有微任务 -> 是否需要渲染 -> 渲染UI

  • 在事件循环中,requestAnimationFrame 实际上就是在UI渲染中执行的

requestAnimationFrame 对比 setTimeout

requestAnimationFrame 由系统决定回调函数的执行时机,不需要使用setTimeout/setlnvertal 去计算刷新时间,节省了不必要的浪费,动画看起来更加流畅。

渲染差异原因分析:
1、在Javascript中, setTimeout属于异步队列,只有当主线程上的任务执行完以后,才会去检查该队列里的任务是否需要开始执行,因此 setTimeout的实际执行时间一般要比其设定的时间晚一些。

2、刷新频率受屏幕分辨率屏幕尺寸的影响,因此不同设备的屏幕刷新频率可能会不同,而 setTimeout只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相同,从而引起丢帧现象。

3、requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次;如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3msrequestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。

转载:www.cnblogs.com/shymi/p/165…

requestAnimatFrame 优点

setTimeout, setlnterval和新生代的 requestAnimation, requestldleCallback🫡

requestldleCallback

  • requestldleCallback 方法在浏览器的空闲时段调用函数排队

setTimeout, setlnterval和新生代的 requestAnimation, requestldleCallback🫡

⏱️ requestldleCallback 的空闲时间是怎么计算的

  • 存在连续渲染的两帧,空闲时间就是帧的频率减去执行任务的时间,减去绘制的时间

setTimeout, setlnterval和新生代的 requestAnimation, requestldleCallback🫡

  • 当一段时间没有绘制或者任务发生,空闲时间将会尽可能变大,但不会超过50ms

setTimeout, setlnterval和新生代的 requestAnimation, requestldleCallback🫡

requestIdleCallback 如何使用

setTimeout, setlnterval和新生代的 requestAnimation, requestldleCallback🫡

以下代码可通过改变 timeout 的设定值来决定requestIdleCallback回调函数的执行时机:

<style>
  .animate-ele{
    width: 50px;
    height: 50px;
    background-color: red;
  }
</style>
<body>
  <div id="animateEle" class="animate-ele"></div>
  <button id="start">开始</button>
  <script>
    //同步耗吋操作
    function syncSleep (duration) {
      const now = Date.now();
      while (now + duration > Date.now()) { }
    }
    const element = document.getElementById('animateEle');
    let count = 0

    function step(timestamp) {
      console.log ("渲染帧");
      count++;
      if (count < 500) {
      element. style.transform = 'translateX(' + count + 'px)';
      window.requestAnimationFrame(step);
      }
    }

    start.onclick = function () {
      console.log ("启动帧")
      // 使用requestAnimationFrame循环调用step是为了让每一帧都有输出
      // 使得每一帧都能让界面有绘制
      window.requestAnimationFrame(step);
      requestIdleCallback ((idleDeadline) => {
        // didTimeout表示是否超时正在执行
        const didTimeout = idleDeadline.didTimeout ? '超时正在执行' : '未超时执行'
        // timeRemaining()表示当前帧还剩余多少时间(以毫秒计算)
        const timeRemaining = idleDeadline.timeRemaining();
        console.log("didTimeout==", didTimeout, "==", timeRemaining)
      },{timeout: 50}); //timeout第二个参数,期望在规定时间内执行回调

      console.log("执行onClick");

      setTimeout (() => {
        console.log("执行timeout");
        syncSleep(1000);
        console.log("执行timeout完成");
        Promise.resolve().then(function () {
          console.log("promise 微任务");
        });
      }, 50)

      syncSleep(1000); // 同步阻塞1s
      console.log("执行onClick完毕");
    }
  </script>
</body>

输出结果:

setTimeout, setlnterval和新生代的 requestAnimation, requestldleCallback🫡

不设置第二个参数 timeout ,输出结果:

setTimeout, setlnterval和新生代的 requestAnimation, requestldleCallback🫡

总结

  • setlnterval 本身存在累计效应

  • setTimeout 存在最低延迟时间;实现动画时,无法与屏幕刷新保持步调一致

  • requestAnimationFrame 系统自动调用,保障刷新频率

  • requestldleCallback 处理低优先级任务空闲时间调用

原文链接:https://juejin.cn/post/7313557322597154855 作者:Dr_哈哈

(0)
上一篇 2023年12月18日 上午10:49
下一篇 2023年12月18日 下午4:06

相关推荐

发表回复

登录后才能评论