一篇文章带你彻底了解定时器

204 阅读5分钟

引言

由于js是一种单线程的且只有一个主线程的编程语言,这就意味着它在同一时间只能执行一个任务。为了让我们可以实现异步操作,js提供了setTimeoutsetInterval两个内置函数来实现延时执行和周期性执行代码片段的能力。本文将深入探讨这两种的工作原理并讲解如何用setTimeout来手写一个setInterval

setTimeout

基本用法

setTimeout 是 JavaScript 中用于设置延时执行的函数。它允许你指定一段代码(通常是一个函数),并在给定的时间延迟后执行这段代码,它的基本用法如下:

const timeoutId = setTimeout(callback, delay);
  • callback:一个函数或可调用对象,将在指定时间后被调用。
  • delay:一个整数,表示多少毫秒(ms)后执行回调函数。

来看一段代码:

setTimeout(function(){
  console.log("-------");
},1000)
console.log(123);

这段代码结果也正如我们所讲,先执行了123,在执行 ------- 。

image.png

异步调用

setTimeout 并不会阻塞主线程的执行,它会将回调函数放入事件队列中等待执行。这是因为 JavaScript 是单线程的,所有任务(包括同步代码和异步回调)都在同一个线程上顺序执行。请看下面这段代码:

setTimeout(function(){
  console.log("-------");
},0)
console.log(123);

我们在这里把延迟时间设置为0了,但打印以后仍然是上面那张图,只不过上面的要等1s才会打印 ------- ,而现在几乎是立马打印的。

取消定时器

setTimeout的返回值是一个定时器 ID (timeoutId),可以用来取消这个定时器(通过 clearTimeout(timeoutId))。如果在计时结束前调用了 clearTimeout,那么回调函数将不会被执行。请看下面这段代码:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>setTimeout Example</title>
</head>
<body>
<button id="stopTimeout">提前关闭setTimeout定时器</button>
<script>
  const stopButton = document.getElementById('stopTimeout');
  let timeoutId;

  // 设置一个10秒后的定时器
  timeoutId = setTimeout(() => {
    console.log('10秒后打印此消息');
  }, 10000);

  // 添加点击事件监听器来提前取消定时器
  stopButton.addEventListener('click', function() {
    clearTimeout(timeoutId);
    console.log('定时器已提前取消');
  });
</script>
</body>
</html>

它的运行结果如下图,当我们点击了提前关闭定时器后,不论等多久它都不会在控制台中打印我们想打印的东西。 image.png

image.png

setInterval

setIntervalsetTimeout 类似,但它不是只执行一次回调函数,而是按照指定的时间间隔重复执行,直到使用 clearInterval(intervalId) 明确停止为止。其基本语法是:

const intervalId = setInterval(callback, delay);
  • callback:一个函数或可调用对象,将以固定的间隔时间被调用。
  • delay:一个整数,表示每次回调之间的等待时间(以毫秒为单位)。

同样地,setInterval 返回一个定时器 ID (intervalId),可用于停止定时器。

取消定时器

下面是setInterval如何取消定时器的代码:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>setInterval Example</title>
</head>
<body>
<button id="startInterval">开始setInterval定时器</button>
<button id="stopInterval">关闭setInterval定时器</button>
<script>
  const startButton = document.getElementById('startInterval');
  const stopButton = document.getElementById('stopInterval');
  let intervalId;

  // 定义要周期性执行的函数
  function periodicFunction() {
    console.log('每秒打印一次');
  }

  // 开始定时器
  startButton.addEventListener('click', function() {
    intervalId = setInterval(periodicFunction, 1000);
    console.log('定时器已启动');
  });

  // 停止定时器
  stopButton.addEventListener('click', function() {
    clearInterval(intervalId);
    console.log('定时器已停止');
  });
</script>
</body>
</html>
image.png

如何用 setTimeout 手写 setInterval

最近听说自己的一个学长在大厂面试中就挂在这道题目上了,笔者正好写一下来熟悉一下,想法应该很简单,我们可以用递归来实现。下面随笔者一起来看看如何实现吧。

1. 定义 customSetInterval 函数

  • 参数

    • fn:要周期性执行的函数。
    • time:每次执行之间的间隔时间(以毫秒为单位)。
  • 内部变量和方法

    • intervalId:存储当前定时器的 ID,用于后续清除定时器。
    • loop():这是一个递归调用自身的辅助函数,它负责设置下一个 setTimeout 并在每次计时结束时调用传入的函数 fn

2. 初始化循环

  • loop() 函数被立即调用一次,开始第一次延迟后执行 fn 的过程。这一步是至关重要的,因为它启动了整个周期性的执行流程。

3. 返回一个清除定时器的方法

  • customSetInterval 返回一个匿名函数,这个函数的作用是清除定时器 (clearInterval(intervalId))。这样做是为了提供一种机制来停止周期性任务。

4. 使用 customSetInterval

  • 创建了一个名为 interval 的变量,它保存了对 customSetInterval 返回的清除函数的引用。这意味着你可以通过调用 interval() 来停止周期性的执行。

5. 停止定时器

  • 使用 setTimeout 设置了一个额外的定时器,在 5 秒后调用 interval() 方法来停止由 customSetInterval 创建的周期性任务。

以下是关键步骤:

  1. 定义一个外部函数:这个函数接受两个参数——一个是要定期执行的回调函数 fn,另一个是每次执行之间的等待时间 time

  2. 创建一个递归调用的辅助函数:这个辅助函数(如代码中的 loop)会在每次调用时重新设置一个新的 setTimeout,并在延迟结束后再次调用自己,从而形成一个连续的执行链。

  3. 初始化第一次延迟:通过立即调用辅助函数来启动第一个延迟计时。

  4. 提供停止定时器的能力:返回一个可以用来清除定时器的函数,以便可以在任何时候停止周期性执行。

  5. 管理定时器 ID:确保你有办法追踪每个定时器的 ID,这样可以在需要的时候正确地清除它们。

注意事项

  • 防止堆栈溢出:由于 loop 是递归调用的,理论上可能会导致 JavaScript 的调用栈溢出。然而,因为每次递归调用都是通过 setTimeout 触发的,而不是直接调用,所以实际上不会发生这种情况。每次递归调用都会在事件循环的下一轮中进行,因此不会累积在调用栈中。

  • 保证异步非阻塞:这种方式确保了即使回调函数 fn 的执行时间超过了设定的时间间隔,也不会影响其他操作或导致浏览器卡顿,因为所有的回调都是异步执行的。

具体代码如下:

function customSetInterval(fn, time) {
      let intervalId = null;
      function loop() {
        intervalId = setTimeout(() => {
          fn();
          loop();
        }, time)
      }
      loop()
      return () => clearInterval(intervalId)
    }

    const interval= customSetInterval(function () {
      console.log('qa');
    }, 1000)

    setTimeout(() => {
      interval()
    }, 5000)

结果如下图:

image.png c224676594aae6804009e3fe521b122.jpg