手写代码 : 使用setTimeout 实现 setInterval 😏 看完这篇 , 稳啦 !

872 阅读7分钟

前言


想要用 setTimeout 实现 setInterval , 有几个关键点需要明白 :

  • setTimeout 基础知识
  • setInterval 基础知识
  • setTimeout 和 setInterval 有什么关系 ?

工欲善其事 , 必先利其器 , 我相信 , 看完这篇文章你一定可以手撕 !

当然 , 如果已经很锋利 , 请各位大佬移步至文末 , 亲自手撕 , 一睹大佬手撕的风采 !

setTimeout & setInterval 对比


这二者都是负责处理延时任务循环任务的 JavaScript 函数,各有各的特点和应用场景:

补充 : requestAnimationFrame:专门用于动画,在下一次重绘之前执行代码,使动画更加平滑和高效。

参数结构

两者都接受相似的参数:

  • setTimeout(callback , time [,arg1,arg2,...])
  • setInterval(callback , time [,arg1,arg2,...])
  1. callback:当计时器到期时要执行的函数。
  2. time:对于 setTimeout 是延迟时间,对于 setInterval 是每次执行之间的间隔时间,以毫秒为单位。非数字值会被视为 0,负数也会被当作 0 处理。
  3. [arg1, arg2, ...] :可选参数,这些参数将在调用回调函数时传递给它。

返回值

  • setTimeout setInterval 都返回一个定时器 ID,这是一个非零整数值,代表已创建的计时器。这个返回值一般用于取消对应的定时器

功能对比

  • setTimeout:用来设定一个计时器,在指定的时间(毫秒)后仅执行一次给定的回调函数。
  • setInterval:类似于 setTimeout,但它会在每隔指定的时间间隔重复执行给定的函数,直到被明确中止 ,适用于需要定期执行的任务。

setTimeout


它很适合在一段时间后执行某个操作。

eg : 模态框的自动关闭,轻松实现一次性的延时任务

要注意的是,时间可能不会精确到设定的毫秒数,因为 JavaScript 是单线的,其他任务也可能会影响到它的执行时间。

举个栗子

setTimeout(() => {
  console.log("This runs once after 1 second.");
}, 1000);

一些其他因素 , 在 1.332 s 后打印

setInterval


再看看 setInterval。这个函数特别适合定时刷新数据轮询服务器或者是做一些每隔一段时间就需要重复的事情

需要注意内存泄漏定时器的清除,最好在使用完之后调用 clearInterval() , 并传入定时器的 ID

(关于这个 clearInterval() 我们在浏览器中 , 做个小实验 😁)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
  </head>
  <body>
    <button id="btn">关闭定时器</button>
    <script>
      const interval=setInterval(function(){
        console.log('hello world');
      },1000)
      
      console.log(interval)
      const btn=document.getElementById('btn');
      btn.onclick=function(){
        clearInterval(interval);
      }

    </script>
  </body>
</html>

在 node 中做个小测试 , 检验下重复性

const intervalId = setInterval(() => {
  console.log("This runs every 2 seconds.");
}, 2000);

// Clear the interval after 10 seconds
setTimeout(() => {
  clearInterval(intervalId);
  console.log("Interval cleared.");
}, 10000);

每隔两秒 , 打印"This runs every 2 seconds." , 10 s 后清除定时器。

补充

最后说说 requestAnimationFrame。这个函数与前两者不同,它是为了提升动画效果而设计的,因为它会在浏览器下一次重绘之前执行回调。这可以使动画更加平滑,与屏幕刷新率同步。适合用来制作高性能动画。

示例代码:

function animate() {
  // 动画代码
  console.log("Animating...");

  // 下一帧继续动画
  requestAnimationFrame(animate);
}

// 开始动画
requestAnimationFrame(animate);

另外,requestAnimationFrame 有个优点是在页面隐藏或最小化时会暂停执行,这对于省电和性能优化都很有好处。

深入理解定时器的运行机制

由于 JavaScript 是单线程语言,它只有一个主线程,所有任务都在这个线程上执行,这意味着一次只能做一件事。为了处理异步操作,比如计时器、网络请求等,JavaScript 采用事件循环(Event Loop)机制 , 当一个异步操作完成时,它会被放入回调队列中等待主线程空闲时再执行。

看下面的栗子 :

<!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>
      setTimeout(function(){
        console.log('-------');
      },10000)

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

执行setTimeout的时候 , 先把callback 函数 放入事件队列 ,当主线程全部执行完后,再来执行放入事件队列中的callback 函数。 所以先打印主线程中的123 , 在打印 "-------" 。

这就是 setTimeout 的异步性 , 因此即使setTimeout()方法指定了一个很短的时间,它也不会在调用代码之后立即执行 , 因为和上面举的栗子一样 , 执行定时器的时候 , 它callback 函数 , 放在事件队列的末尾,直到事件队列中没有任何待处理的任务,才会执行。

因此,我们可以知道 , 当代码块执行时,当前执行的上下文(也称为堆栈)已经被清空。

如果setTimeout() 方法在代码块在执行之前被清除或者代码块执行时间过长,那么代码块将会在JavaScript引擎空闲时尽快被执行。

<!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">停止</button>
    <script>
      setTimeout(function(){
        console.log('hello world');
      },1000);
      while(true){
        
      }
    </script>
  </body>
</html>

差点把我浏览器搞崩溃了 ......

这个例子也说明 , setTimeout()方法不是一个精确的时间控制器,而是一个粗略的时间控制器。如果需要更精确的时间控制器,可以考虑使用 requestAnimationFrame()或 Web Workers.

接下来就去手撕面试题吧 🤡 !

题目


用 setTimeout 实现 setInterval

题目分析

在 JavaScript 中,setInterval 和 setTimeout 是用于管理时间的两个基本方法。

  • setInterval 会按照固定的时间间隔反复执行某个函数,
  • 而 setTimeout 在指定时间后执行一次指定的函数。

在这道题中,要求使用 setTimeout 来模拟 setInterval 的功能。

因此,我们需要让 setTimeout 在其回调函数中再次调用自身,从而实现类似 setInterval 不断调用的效果。

实现思路

1) 定义一个函数,该函数将执行我们需要周期性运行的任务。

2) 在这个函数内,先执行任务逻辑,然后使用 setTimeout 调用自身,再次设定下一次执行的时间。

3) 将初始调用放在一个主函数(或直接调用该函数),从而触发首次定时操作。

实现代码

function customSetInterval(callback, interval) {
    function intervalFunction() {
      callback();
      setTimeout(intervalFunction, interval);
    }
    setTimeout(intervalFunction, interval);
  }
  

  var f = () => {
    console.log('模拟的 setInterval,定时任务执行中...');
  }

  // 使用 customSetInterval 模拟 setInterval
  customSetInterval(f, 1000);

代码解释:

1) 定义了名为 customSetInterval 的函数,该函数接收两个参数:callback 和 interval。callback 是要周期性执行的任务,interval 是两次任务执行之间的时间间隔。

2) 在 customSetInterval 内部,定义了一个名为 intervalFunction 的函数。在 intervalFunction 中,首先调用 callback 函数来执行具体的任务,然后使用 setTimeout 调用 intervalFunction 本身,并设定相同的时间间隔 interval。这使得 intervalFunction 可以不断循环执行,从而模拟 setInterval 的效果。

3) 在 customSetInterval 的最后,首次调用 setTimeout(intervalFunction, interval) 来启动循环。

另一种写法 :

 function customSetTimeout(fn,time){
        let intervalID=null
        function loop(){
          intervalID= setTimeout(()=>{
            fn();
            loop();
          },time)
        }
        loop();
        return ()=>clearTimeout(intervalID);
      }
      const interval=customSetTimeout(function(){
        console.log('hello world')
      },1000)

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

总结

虽然 setTimeout 可以用来模拟 setInterval,但两者有一些不同之处,了解这些差异有助于更好地使用定时器功能。

1) setInterval 的确切时间间隔:setInterval 是在任务开始执行的时间点计时,因此如果任务执行时间较长,会影响下一个任务的时间间隔。从而导致任务执行频率降低。

2) setTimeout 的灵活性:使用 setTimeout 模拟 setInterval 更具灵活性,因为每次递归调用可以动态调整时间间隔。例如,如果需要在任务执行后动态决定下一次执行的时间,可以根据业务逻辑调整 setTimeout 的时间参数。

3) 清除定时器:无论使用 setInterval 还是 setTimeout,清除定时器对性能优化以及防止内存泄漏都是非常重要的一个功能。

  • 对于 setInterval,使clearInterval;
  • 对于 setTimeout,使用 clearTimeout。

阁下 , 手撕否 ? 请在评论区说一声呵呵 🤡