聊聊定时器 setTimeout和setInterval 时延问题

474 阅读4分钟

之前在写vue处理事件延迟执行,一般会用到setTimeout,而很少关注到setInterval 当然面试时候,如果简单问一下setTimeout可能会答,但是问到和setInterval联系和区别可能不得而知。下文就来聊聊定时器 setTimeout和setInterval 时延问题

引入

下面代码中,设置定时器time为0,会立即执行吗?

  <script>
    setTimeout(function(){
        console.log("hello world");
    },0)
    console.log("先执行");
  </script>

答案是不会,那为什么?

实际上,即使我们将 setTimeout 的延迟时间设置为 0 毫秒,回调函数也不会立刻执行。这是因为 JavaScript 是单线程语言,所有的同步任务都会在一个叫做 调用栈 的结构中依次执行,直到调用栈为空,才会去检查 事件队列 中是否有待处理的任务

setTimeout 的时间到达(即使是 0 毫秒),它的回调函数会被放入 宏任务队列 中,而不是直接加入到调用栈中执行。只有当当前的同步代码完全执行完毕,即调用栈清空后,JavaScript 引擎才会从宏任务队列中取出第一个任务并执行。因此,在上面的例子中,console.log("先执行") 会先于 console.log("hello world") 被打印出来,因为它是同步代码的一部分,而 setTimeout 的回调则属于异步任务。

如果你不了解事件队列这方面的,这里有面试题有关事件处理和Promise详细解析

setTimeout VS setInterval

让我们先来区分一下 setTimeoutsetInterval 这两个定时器函数的作用:

  • setTimeout:这是一个一次性定时器,它会在指定的延迟时间之后执行一次给定的回调函数。如果需要停止定时器,可以使用 clearTimeout 函数,并传入由 setTimeout 返回的定时器 ID。例如,你可以提供一个按钮让用户点击以终止定时器的执行。
  • setInterval:这是一个循环定时器,它会按照设定的时间间隔重复执行同一个回调函数,直到你显式地调用 clearInterval 来停止它。需要注意的是,setInterval 可能会在前一个回调还未完成时就再次触发新的回调,这可能会导致一些意想不到的行为,比如多个回调堆积在一起快速执行

看下面代码进行加深理解:

   <button id="btn">提前关闭setTimeout定时器</button>
   <button id="btn1">关闭setInterval循环定时器</button>
   <script>
      const btn = document.getElementById('btn');
      btn.addEventListener('click',()=>{
          clearTimeout(settimeout)
      })
      const settimeout = setTimeout(()=>{
          console.log('我是一次性定时器')
      },4000)

      const btn1 = document.getElementById('btn1');
      btn1.addEventListener('click',()=>{
          clearInterval(interval)
      })
      const interval = setInterval(()=>{
          console.log('我是循环定时器')
      },1000)
   </script>

4s后“一次执行事件”结束。“循环”事件续上。只点击“关闭setInterval”事件,“循环事件”不再执行。 image.png 点击“提前关闭setTimeout定时器”,4s后不执行“setTimeout”事件 image.png

使用 setTimeout 模拟 setInterval 的实现方法

有时我们希望规避 setInterval 的一些潜在缺点,例如它不会等待前一个回调函数执行完毕再启动下一个,而是严格根据设定的时间间隔触发,这可能导致回调函数的累积调用,尤其是在回调函数执行时间超过设定间隔时。

为了解决这一问题,我们可以利用 setTimeout 通过递归的方式来模拟 setInterval 的行为,确保每次回调都是在上一次回调完成后才开始计时

<script>
    /**
     * 使用 setTimeout 实现类似 setInterval 的功能
     * @param {Function} fn - 要执行的回调函数
     * @param {Number} time - 两次回调之间的间隔时间(毫秒)
     * @returns {Function} - 用于清除定时器的函数
     */
    function customSetInterval(fn, time) {
        let timerId;
        function loop() { // 定义递归调用的内部函数
            timerId = setTimeout(() => {
                try {
                    fn(); // 执行传入的回调函数
                } catch (error) {
                    console.error('回调函数执行出错:', error);
                }
                loop(); // 递归调用以保持循环
            }, time);
        }
        loop(); // 启动第一次调用
        return () => {  // 返回一个函数,用于停止定时器
            clearTimeout(timerId);
            console.log('定时器已关闭');
        };
    }
    const interval = customSetInterval(() => {    // 创建一个自定义的循环定时器
        console.log("Hello, world!");
    }, 1000);

    // 在5秒后关闭定时器
    setTimeout(() => {
        interval();
    }, 5000);
</script>

这种方法不仅能够保证每个回调都在前一个回调完成后才开始计时,而且还允许我们在必要时优雅地结束定时器,避免了 setInterval 可能带来的累积问题。

优雅结束~~

12.webp