“你会使用 setTimeout 来实现 setInterval 吗?”来自于面试官的灵魂拷问

2,771 阅读5分钟

前言:

上一世,我站在了梦想的门前——一家心仪已久的大厂就在眼前。然而,命运似乎喜欢开玩笑,在最后一轮面试中,面试官抛出了一个看似简单却足以改变一切的问题:“你能用 setTimeout 实现 setInterval 吗?”那一瞬间,我的心跳仿佛停滞,脑海中一片空白。最后,我只能眼睁睁地看着这份机会溜走,带着遗憾和不甘,噢不!我的大厂梦寄你太美了。

屏幕录制 2024-12-05 124213.gif

这一世,我重活一遍,决不能在这里倒下,面试官,你才是挑战者!

简述:

本文将详细讲解:定时器中的setTimeoutsetInterval,以及它们的运行顺序,和最后的压轴面试题:使用 setTimeout 来实现 setInterval 。我会先介绍完基础知识,最后连串起来手撕面试题,如果兄弟们等不及的可以直接跳到最后看如何实现。

定时器:

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

setTimeout 和 setInterval 函数:

之所以把它们放在一起讲,是因为其实它的功能有点相似,一起讲便于大家更好的理解。

功能对比
  • setTimeout:用来设定一个计时器,在指定的时间(毫秒)后仅执行一次给定的回调函数。
  • setInterval:类似于 setTimeout,但它会在每隔指定的时间间隔重复执行给定的函数,适用于需要定期执行的任务。
参数结构

两者都接受相似的参数:

  1. callback:当计时器到期时要执行的函数。
  2. time:对于 setTimeout 是延迟时间,对于 setInterval 是每次执行之间的间隔时间,以毫秒为单位。非数字值会被视为 0,负数也会被当作 0 处理。
  3. [arg1, arg2, ...] :可选参数,这些参数将在调用回调函数时传递给它。
返回值
  • setTimeout 和 setInterval 都返回一个定时器 ID,这是一个非零整数值,代表已创建的计时器。这个返回值一般用于取消对应的定时器

如何理解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>
       setTimeout(function(){
            console.log('hello world');
         },10000)
         
         console.log(123)
      </script>
</body>
</html>

大家可以猜猜结果:

123 hello world

解释: 兄弟们肯定有疑问,为什么不是先执行定时器,再输出123呢?这样的想法也没错,JS执行机制:从上到下嘛。但是换个思想,假如说:定时器的时间是接近于无限长,如果先执行定时器,那么后面的程序是不是永远不可能得到运行了。

显然开发者不会允许这种情况发生,那么setTimeout,它的运行其实是异步的,也就不是从上到下,而是先把callback 函数 放入 event loop,然后当主线程全部执行完后,再来执行放入 event loop中的callback 函数

所以也就引出了一个问题:是否定时器会按时输出函数中的内容呢?

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

答案当然是否定的!为什么呢?

  1. 当你的主线程程序是无限次的循环时,那么主线程没有结束,就不会去执行定时器中的回调函数了。

比如:

<!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>
  1. 就是如果你在网页中打开,定时器启动但是你在未达到定时器时间时,就关闭了网页,这样也执行不了。

如何关闭定时器呢?

关闭定时器,这时候就要用到定时器自带的返回值了。

<!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>

运行以下代码,我们可以看到结果:

屏幕录制 2024-12-05 1001 -original-original.gif 我们可以看到,当没点击停止按钮时,hellow world一直在输出,点击后就停止了。

流程分析:

  1. 设置定时器:在<script>标签内的代码被执行时,首先通过setInterval函数创建了一个定时器。这个定时器每秒(1000毫秒)都会触发一次匿名函数,该匿名函数的作用是向浏览器的控制台输出字符串'hello world'。setInterval返回一个唯一的标识符(ID),用来标识这个定时器,这里被存储在变量interval中。
  2. 打印定时器ID:紧接着,console.log(interval)这行代码将定时器的ID输出到控制台。
  3. 获取按钮元素:使用document.getElementById('btn')获取页面上的按钮元素,并将其赋值给btn变量。
  4. 为按钮添加点击事件处理器:为btn按钮设置了点击事件处理函数。当用户点击按钮时,会调用clearInterval(interval)来停止定时器,从而阻止'hello world'继续被输出到控制台。
  5. 用户交互:一旦页面加载完毕,如果用户点击了“关闭定时器”按钮,就会触发上述的点击事件处理函数,定时器将会被清除,之后就不会再有'hello world'的消息输出到控制台了。

当我们拿捏了以上的知识点,我们就可以开始解面试题了。

面试题

如何 setTimeout 实现 setInterval

首先我们来分析一下:这是一道场景编程题,要求我们去使用setTimeout,然后实现setInterval,当时我看到这题的时候,想到的是使用setTimeout去递归自己,但是好像不太行。

好了来上代码吧!

<!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>
    <script>
     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)

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

流程分析:

  1. 定义customSetTimeout函数
  • customSetTimeout接受两个参数:一个回调函数fn和一个时间间隔time(以毫秒为单位)。
  • 定义了一个内部变量intervalID,用于存储当前setTimeout的ID。
  • loop函数是一个递归函数,它设置一个setTimeout,在指定的时间间隔后执行传入的fn函数,并再次调用自身以保持循环。
  1. 启动首次循环
  • customSetTimeout被调用时,它立即调用了loop(),这会开始第一次定时器计时。当定时器到期时,fn将被执行,并且loop会再次被调用,形成一个无限循环。
  1. 返回清除定时器的函数
  • 最后的返回值是点睛之笔:返回了一个函数基于当前setTimeout的ID,而关闭相应的定时器的函数。运行该函数,从而终止循环,
  1. 使用customSetTimeout函数
  • 创建了一个名为interval的常量,它实际上是指向customSetTimeout返回的清除定时器函数的引用。
  • 使用customSetTimeout设置了每秒执行一次的定时任务,每次执行时都会输出'hello world'到控制台。
  1. 设定定时器以停止customSetTimeout
  • 使用setTimeout设置了一个新的定时器,在5秒(5000毫秒)后触发。
  • 当这个新的定时器到期时,它会调用interval(),即之前由customSetTimeout返回的函数,用来清除由customSetTimeout创建的无限循环,终止定时任务。

总结:

如此一来,我们就实现了使用使用 setTimeout 来实现 setInterval,并且定时的关闭了这个功能。

结语:

分享知识的时间总是短暂的,如果对你有帮助,也请给我点个赞吧!