面试官:如何用setTimeout实现setInterval?

614 阅读4分钟

听说有个学长面试被面试官拷打了这个问题,吓得我赶紧写篇博客压压惊。

setTimeoutsetInterval 是 JavaScript 中用于定时执行代码的两个重要函数。它们允许你指定一段代码(通常是一个函数)在经过一定延迟后执行,或者每隔一定时间间隔重复执行。这两个函数都属于浏览器的 Web API,而不是 JavaScript 语言本身的一部分。

1. 基础知识

1.1 setTimeout

setTimeout 用来设置一个计时器,在指定的时间毫秒数(ms)之后执行一次给定的回调函数。

let timeoutID = setTimeout(callback, delay, [param1, param2, ...]);
  • callback:这是将要执行的函数或代码字符串。
  • delay:这是一个数值,表示多少毫秒后执行回调函数。如果这个值为0或被省略,那么回调函数将会尽可能快地被执行,但不是立即执行,因为它是异步的。
  • [param1, param2, ...]:这些是可选参数,会作为参数传递给回调函数。

返回值是一个唯一的标识符 (timeoutID),你可以使用它来取消定时器,通过调用 clearTimeout(timeoutID)

1.2 setInterval

setInterval 类似于 setTimeout,但它不是只执行一次回调函数,而是按照指定的时间间隔(delay)不断重复执行,直到你显式地停止它。

let intervalID = setInterval(callback, delay, [param1, param2, ...]);
  • 参数和返回值与 setTimeout 相同,但是 setInterval 会在每次延迟结束后重复调用回调函数,直到你调用 clearInterval(intervalID) 来停止它。

1.3 单线程与异步执行

JavaScript 是单线程语言,意味着它只有一个主线程来处理任务。这使得同一时间只能执行一个任务,而其他的任务必须等待当前任务完成。对于 setTimeout 来说,它是异步执行的,这意味着当调用 setTimeout 时,它会将回调函数安排在未来某个时间点执行,但这个回调函数会被放入事件循环(event loop)中。因此,主线程将继续执行后续的同步代码,只有当所有同步任务都执行完毕后,事件循环才会开始检查并执行已到时间的定时器回调。

// 异步任务
const timeout = setTimeout(function(){console.log('456')},5000) // 不会执行
console.log(123)
// 同步任务 阻塞线程
while(true){
console.log('123')
}         

2. setTimeout -> setInterval 代码实现

2.1 定义 customSetInterval 函数

function customSetInterval(fn, t) {
    let intervalId = null;
    
    function loop() {
        intervalId = setTimeout(() => { fn(); loop(); }, t);
    }
    
    loop();
    
    return () => clearTimeout(intervalId);
}
  • 参数:

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

    • intervalId: 用来存储 setTimeout 的返回值,即计时器的ID。这个ID可以用来取消定时器。
    • loop(): 这是一个递归函数,它会首先调用传入的 fn 函数,然后再次设置一个 setTimeout 来安排下一次的 loop() 调用,从而形成循环。
  • 启动与返回:

    • 当 customSetInterval 被调用时,它立即调用了 loop() 方法开始第一次执行。
    • 最后,customSetInterval 返回一个匿名函数,这个函数的作用是清除当前的 intervalId,以此来停止定时器。

2.2 创建并运行定时器

const interval = customSetInterval(function () { console.log(123); }, 1000);
  • 这行代码创建了一个新的定时器,它会每秒打印数字 123 到控制台。interval 是指向清除定时器的函数的引用。

2.3 停止定时器

setTimeout(() => { interval(); }, 5000);
  • 这里使用了 setTimeout 来安排在5秒后执行 interval 函数,这将清除由 customSetInterval 创建的定时器,从而停止每秒打印 123 的行为。

2.4 注意事项

  • 内存管理: 在这里,customSetInterval 返回了一个清除定时器的方法,使得我们可以显式地停止定时器,避免不必要的资源占用。
  • 非精确性: 尽管我们设定了1000毫秒的间隔,但由于 JavaScript 的单线程性质以及事件循环的工作方式,实际的执行时间可能会有所偏差。
  • 递归调用loop() 函数通过递归的方式不断调用自己,这种方式可以有效模拟 setInterval 的行为,但需要注意的是,如果回调函数执行的时间超过了设定的时间间隔(t),那么下一次回调的执行将会被推迟。

2.5 源码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>setTimeout->setInterval</title>
</head>
<body>
    <script>
        function customSetInterval(fn,t){
            let intervalId = null;
            function loop(){
                intervalId = setTimeout(()=>{fn();loop();},t)
            }
            loop();
            return ()=> clearTimeout(intervalId)
        }

       const interval = customSetInterval(function(){console.log(123)},1000)

       setTimeout(()=>{interval()},5000)
    </script>
</body>
</html>

3.总结

通过以上方法我们可以实现使用setTimeout实现setInterval,主要是通过递归来模拟setInterval的相关功能。怎么说,准备好面对面试官的拷打了吗?