你还不了解JS定时器吗?那么它来了

208 阅读5分钟

深入理解 JavaScript 定时器:setTimeout 与 setInterval

JavaScript 是一种单线程语言,其执行模型基于事件循环(event loop),这意味着它在同一时间只能做一件事。为了处理异步操作,如网络请求、用户交互和定时任务,JavaScript 使用了回调函数和事件队列机制。

单线程与事件循环

由于 JavaScript 的单线程特性,所有的同步代码都在主线程上执行。而像 setTimeoutsetInterval 这样的异步计时器,则不会堵塞主线程的执行。当指定的时间到了,它们会将回调函数放入事件队列(宏任务(Macrotask)队列微任务(Microtask)队列)中,等待主线程空闲时再执行这些回调函数。

setTimeout 与 setInterval 的行为

  • setTimeout:这是一个一次性定时器,用于在指定的延迟后执行一次回调函数。它接受两个参数:要执行的回调函数和延迟的时间(以毫秒为单位)。回调函数会被放入事件循环中,在当前调用栈清空后执行。
  • setInterval:这是周期性定时器,用于每隔固定的时间间隔执行一次回调函数。同样地,它也不会阻塞主线程,并且每次执行都会将回调函数放入事件队列中。

示例:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>定时器</title>
    </head>
    <body>
        <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('我是一次性定时器')
           },1000)

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

运行结果如下:

image.png

定时器一定会在指定时间后执行吗?

理论上,setTimeoutsetInterval 应该在设定的时间之后触发回调函数。然而,实际执行时间可能会稍有延迟,原因如下:

  1. 事件循环的性质:如果主线程正在忙于执行其他代码,那么即使计时器到期,回调函数也必须等到当前执行栈为空才能被执行。
  2. 计时精度:浏览器或 JavaScript 引擎对计时器的实现可能有一定的最小分辨率,这可能导致轻微的时间偏差。
  3. 浏览器优化:某些浏览器会对多个定时器进行合并(例如,在标签页失去焦点时),以减少资源消耗。

因此,虽然我们期望定时器在精确的时间点触发,但实际执行时间可能会有所延迟, 不同环境下的最小分辨率可能有所不同:

  • 浏览器环境:大多数现代浏览器对 setTimeoutsetInterval 的最小分辨率是 4毫秒。这是由 HTML5 标准规定的,对于重复调用的定时器(如 setInterval 或连续调用 setTimeout),如果指定的时间间隔小于4毫秒,则会被自动调整为至少4毫秒。
  • Node.js 环境:在 Node.js 中,最小分辨率通常是 1毫秒,但在某些情况下也可能更高,具体取决于操作系统的调度机制和系统负载。

如何用 setTimeout 实现 setInterval

既然 setInterval 可以通过周期性地将回调函数推入事件队列来模拟重复执行,那么我们可以使用 setTimeout 来手工构建类似的行为。下面是一个简单的实现,展示了如何创建一个自定义的 setInterval 功能:

function customSetInterval(callback, time) {
    let intervalId = null;

    function loop() {
        intervalId = setTimeout(() => {
            callback();
            loop(); // 递归调用以保持循环
        }, time);
    }

    loop();

    return () => clearTimeout(intervalId); // 返回一个清除定时器的函数
}

// 使用示例:
const interval = customSetInterval(() => {
    console.log('周期性任务');
}, 1000);

// 停止定时器
setTimeout(() => {
    interval();
    console.log('定时器已停止');
}, 5000);

问题思考:

1.这里传入的回调函数callback是哪个?

2.这里递归调用loop()函数,每次创建一个新的setTimeout(),其ID会不会修改。

解释:

1.这里传入的回调函数为:

() => {
    console.log('周期性任务');
}

2. 关键点:intervalId 的更新

  • 初始状态intervalId 初始化为 null
  • 首次调用 loop() :第一次调用 setTimeout 时,intervalId 被设置为新的定时器 ID。
  • 递归调用:在每次回调函数执行完毕后,loop() 再次被调用,并且 intervalId 被重新赋值为新的 setTimeout 的 ID。

因此,虽然 intervalId 变量本身在整个函数作用域内是同一个变量,但它保存的值(即定时器 ID)在每次递归调用中都会被更新为最新的 setTimeout 的 ID。

完整代码解释:

  • 初始状态: 调用customSetInterval()函数,函数首次执行loop()函数,创建setTimeout时,intervalId被赋予新的ID值。
  • 递归调用: 根据传入的time,这里time就是setTimeout里面的loop()函数被递归调用的时间间隔。每次递归调用后intervalId就会被赋予新的值。
  • 返回清除函数: 这里返回的() => clearTimeout(intervalId);里面的intervalId就是最近被赋予的ID值,5秒后,外部的 setTimeout 触发,调用 interval(),这实际上是调用了 clearTimeout(intervalId),从而取消了当前最新的定时器,阻止了下一次回调函数的执行。

总结

通过理解和利用 JavaScript 的单线程特性和事件循环机制,我们可以更灵活地控制异步操作,包括创建自己的定时器逻辑。无论是使用内置的 setTimeoutsetInterval,还是手写实现类似的定时器功能,掌握这些概念对于编写高效且响应迅速的应用程序至关重要。

希望这篇文章能够帮助你更好地理解 JavaScript 中的定时器机制,并为你提供实用的知识来解决日常编程中的问题。如果你有任何疑问或需要进一步的帮助,请随时提问!