定时器:JavaScript 中的异步魔法

109 阅读5分钟

前言

在 JavaScript 开发的世界里,定时器就像是我们的小闹钟和魔法时钟一样,不可或缺。无论是给动画加点“心跳”、定期检查数据还是轻轻延后某个动作,定时器都能帮我们轻松搞定一切任务。本文将带你一起深入探索 JavaScript 中这些神奇的定时器机制,并展示如何用 setTimeout 实现类似 setInterval 的小把戏!

什么是定时器?

定时器允许我们在指定的时间点或时间间隔后执行特定的代码。JavaScript 提供了两种主要的定时器函数:

  1. setTimeout

    • 在指定的延迟时间后执行一次回调函数。
    • 基本语法:setTimeout(callback, delay)
  2. setInterval

    • 每隔指定的时间间隔重复执行回调函数。
    • 基本语法:setInterval(callback, interval)

JavaScript 是单线程的

JavaScript 是一种单线程的语言,这意味着在同一时间只能执行一个任务。所有的同步和异步操作都在这个唯一的主线程上进行。这种特性决定了 JavaScript 如何处理定时器。

异步执行与事件循环

  • 异步执行

    • setTimeout 和 setInterval 是异步函数,它们不会阻塞主线程。
    • 当调用 setTimeout 或 setInterval 时,回调函数会被放入事件队列中,等待主线程空闲时再执行。 这意味着如果主线程上有其他同步代码正在执行,这些同步代码会先被执行完毕,然后才会处理事件队列中的回调函数。
  • 事件循环(Event loop)

    • 事件循环是一个不断检查调用栈是否为空的过程。
    • 如果调用栈为空,事件循环会从事件队列中取出第一个任务并推送到调用栈中执行。

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

  • 不一定。虽然 setTimeout 设置了一个最小延迟时间,但由于 JavaScript 是单线程的,如果主线程上的同步代码执行时间超过了设定的延迟时间,那么回调函数的实际执行时间可能会比预期的晚。
  • 例如,如果你设置 setTimeout(callback, 1000),但实际上主线程上的同步代码执行了 1500 毫秒,那么回调函数将在大约 1500 毫秒后才开始执行。

为什么 setTimeout 延迟为 0 仍然不会立即执行:

  • 虽然 setTimeout 的延迟时间为 0 毫秒,但这只是表示引擎尽快将回调函数放入事件队列。
  • 回调函数仍需等待当前调用栈中的所有同步代码执行完毕后才能被执行。
  • 因此,在例子中,console.log(123) 会在 setTimeout 的回调函数之前执行。

下面是一个具体的示例来说明这一点:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>setTimeout Example</title>
</head>
<body>
    <script>
        setTimeout(function () {
            console.log("----");
        }, 0);

        console.log(123);
    </script>
</body>
</html>

在这个示例中,控制台的输出将会是:

123
----

这表明 console.log(123) 先于 console.log("----") 执行,因为 setTimeout 的回调函数需要等待当前调用栈中的同步代码执行完毕后才能被执行。

总结来说,setTimeout 的回调函数总是会被放入事件队列,并在主线程空闲时执行,因此即使设置了 0 毫秒的延迟,它也不会立即中断当前的同步代码执行。

如何取消定时器setTimeout?

  1. 如果你想取消一个通过 setTimeout 设置的定时器,可以使用 clearTimeout(timeoutId) 方法。timeoutId 是调用 setTimeout 时返回的一个标识符。
let timeoutId = setTimeout(() => {
  console.log("By order of the Peaky Blinders");
}, 1000);

clearTimeout(timeoutId); // 取消定时器
  1. 下面是一个简单的示例,展示了使用 button 来取消定时器 setTimeoutsetInterval 的基本用法:
<!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="btn2">提前关闭 setTimeout 定时器</button>
    <button id="btn">关闭 setInterval 定时器 </button>
    <script>
        const btn = document.getElementById('btn');
        const btn2 = document.getElementById('btn2');

        btn2.addEventListener('click', function () {
            clearTimeout(timeout);
        });

        btn.addEventListener('click', function () {
            clearInterval(interval);
        });

        const timeout = setTimeout(function () {
            console.log("----");
        }, 10000);

        const interval = setInterval(function() {
        }, 1000);

        console.log(123);
    </script>
</body>
</html>

在这个示例中:

  1. console.log(123); 会立即执行。
  2. setTimeout 的回调函数会在 10 秒后执行。
  3. setInterval 的回调函数每隔 1 秒执行一次。
  4. 点击按钮可以分别清除 setTimeout 和 setInterval

使用 setTimeout 实现 setInterval——递归

虽然 setInterval 可以方便地实现周期性执行,但我们也可以手动使用 setTimeout 来实现相同的功能。这种方法可以帮助我们更好地理解定时器的工作原理。

自定义 setInterval

我们可以编写一个名为 customSetInterval 的函数,它接受一个回调函数和一个时间间隔作为参数,并使用递归的方式模拟 setInterval 的行为。

实现步骤

  1. 定义 customSetInterval 函数

    • 接受两个参数:回调函数 fn 和时间间隔 time
    • 返回一个取消定时器的方法。
  2. 内部递归函数

    • 使用 setTimeout 调用回调函数。
    • 在回调函数执行后再次调用自身,形成递归。
  3. 取消定时器

    • 返回一个方法,用于清除当前的 setTimeout,从而停止递归。

示例代码

<!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>
    <script>
        function customSetInterval(fn, time) {  //接受两个参数:回调函数 `fn` 和时间间隔 `time`
            let intervalId = null; // 定义一个为null的标识符

            function loop() {
                intervalId =setTimeout(() => {
                fn();
                loop();
              }, time)
            }
            loop();

            return () => clearTimeout(intervalId); // 返回一个取消定时器的方法
        }

        const interval = customSetInterval(() => {
            console.log('By order of the Peaky Blinders');
        }, 1000);

        setTimeout(() => {
            interval(); // 关闭自定义的定时器
        }, 5000);
    </script>
</body>
</html>

在这个示例中:

  1. customSetInterval 函数会每隔 1 秒打印一次 "By order of the Peaky Blinders"。
  2. 5 秒后,通过调用返回的取消方法来停止定时器。

总结

通过本文,我们深入了解了 JavaScript 中定时器的工作原理,包括 setTimeoutsetInterval 的区别以及它们在事件循环中的角色。此外,我们还学会了如何使用 setTimeout 实现类似于 setInterval 的功能,这不仅增强了我们的编程能力,也让我们对 JavaScript 的异步机制有了更深刻的理解。