• 作者:老汪软件技巧
  • 发表时间:2024-12-06 00:09
  • 浏览量:

前言:

最近在听大佬面试经历时,得知他遇到了一道“上古“面试题,即:如何用 setTimeout 实现 setInterval?我当时一听,坏了,我对于定时器有初步的了解,但确实没有对其进行深挖,所以本篇文章将讲解何为setTimeout及其作用,并且解决这道面试题,话不多说,直接开学!

JavaScript 定时器概述JavaScript的单线程特性

JavaScript 是一种单线程语言,这意味着它在同一时间只能执行一个任务(只有一个主线程)。这个单一的线程用于处理所有的代码执行、事件处理和UI更新。由于这种单线程特性,如果一段代码需要长时间运行(例如复杂的计算或等待I/O操作),它将会阻塞整个程序,导致页面无响应。

异步操作的重要性

所以为了克服单线程的局限性并提高用户体验,JavaScript 支持异步操作。异步操作允许程序启动某些可能耗时的任务(如网络请求、文件读写等),而不阻塞主线程。一旦这些任务完成,它们会将结果排队,等待时机成熟再由主线程处理。这种方式使得应用程序可以保持响应,即使有后台任务正在进行。

setTimeout 详解

而由于上述俩个 JavaScript 的特性,就诞生了 setTimeout ,其是JavaScript 中用于延迟执行代码的内置函数,我们可以通过 setTimeout指定一串代码在一段时间后执行。

// 模板
let timerID = setTimeout(callback, delay, arg1, arg2, /* ... */);
// callback 函数 + delay(毫秒 ms)会放入 event loop 队列中

所以,setTimeout 是异步执行的计时器,但是其还有一个十分重要的性质,即:会在主线程执行完后才会执行,这句话是什么意思?我们不妨来看几个例子:

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>
        const timeout = setTimeout(function() {
            console.log('----')
        },1000)
        // 同步代码
        console.log(123)
    script>
body>
html>

这是一个简单的通过使用setTimeout实现在等待一秒后打印"----",我们来看看输出结果。

7b0e8f3e0cd487d4ebec0f7ae0a7d00.png

但是当我们在中间添加一个死循环会发生什么?定时器会在到达指定时间后执行吗?

// 在script部分
const timeout = setTimeout(function() {
    console.log('----')
},1000)
while(true) {} 
// 同步代码
console.log(123)

当我们打开页面后就会发现,我们无法对页面执行任何操作,这是因为单线程的缘故,死循环阻塞了整个程序,导致页面无响应。

但是我们想,定时器是在死循环之前执行的,到点了应该去执行回调函数呀?但是却什么都没有发生,这就是因为setTimeout会在主线程执行完后才会执行,主线程已经进入死循环了,那么也就轮不到回调函数了。

回调函数和延迟时间参数

当我们调用 setTimeout 时,需要提供一个 回调函数 作为第一个参数。这个回调函数可以是 匿名函数、命名函数或者是任何可调用的对象。延迟时间是以毫秒为单位的数字,表示在尝试执行回调之前需要等待的时间。例如:

setTimeout(() => {
    console.log('This will be printed after 3 seconds.');
}, 3000); // 3秒后执行

那么聪明读者就会想了,哎~我们将时间设置为0,不就可以直接执行吗?例如:

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

在执行后我们就会发现,仍然是之前的结果

7b0e8f3e0cd487d4ebec0f7ae0a7d00.png

这是因为主线程的执行时间可能会超过指定时间,而由于其在主线程执行完后才会执行的特性,所以setTimeout实际的执行时间 > 指定时间,哪怕设置时间为0,也可能产生1~5ms的延迟。

获取和理解定时器ID

当我们调用 setTimeout 或 setInterval 时,JavaScript 引擎会返回一个唯一的定时器ID。这个ID用于标识你创建的特定定时器,我们可以通过这个ID来管理和控制定时器的行为。

let timeout = setTimeout(() => {
    console.log('----')
}, 1000); // 返回一个定时器ID
console.log(timeout); // 输出 1 或其他数字,表示定时器ID

这个 ID 十分重要,因为我们需要通过这个 ID 在后续代码中引用和操作这个定时器,如果丢失了这个ID,我们就无法直接取消或管理该定时器。

使用定时器ID取消定时器(clearTimeout)

如果我们需要在定时器到点前停止它的执行,我们可以使用 clearTimeout 函数,并传入相应的 ID 作为参数,就可以立即取消与该ID关联的定时器,其回调函数也就不会被执行。例如:

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="btn">关闭定时器button>
    <button id="btn2">提前关闭setTimeout定时器button>
    <script>
        const btn = document.getElementById('btn')
        const btn2 = document.getElementById('btn2')
        // click 事件 是异步的
        btn.addEventListener('click',function() {
            clearInterval(interval)
        })
        btn2.addEventListener('click',function() {
            clearInterval(timeout)
        })
        const timeout = setTimeout(function() {
            console.log('----')
        },10000)
        const interval = setInterval(function() {
            console.log('xxx')
        },1000)
    script>
body>
html>

在上述代码中,我们创建了两个按钮用于管理定时器,在未关闭定时器时会执行

image.png

而当我们在10s前点击”提前关闭setTimeout定时器“,那么就不会打印”----“。而也可以通过点击”关闭定时器“来结束setInterval的任务。

image.png

在setInterval执行11次后(即运行了11s)也没有进行打印"----",说明setTimeout已经结束了,而在点击”关闭定时器“后,setInterval也不再执行。

一句话总结setTimeout

在了解了上述setTimeout的性质后,我们就可以向面试官解释什么是setTimeout:

setTimeout 是 JavaScript 的异步计时器,它在接受一个回调函数和延迟时间(毫秒)作为参数后,将该回调加入事件循环队列,会在主线程执行完后才会执行,实际执行时间可能因主线程的繁忙程度而晚于指定时间,可以通过setTimeout 返回的定时器ID可取消执行。

setTimeout 遇上 setInterval:一场关于时间的趣味对决 ✨_趣味对抗赛_

用 setTimeout 实现 setInterval:

在进行实现之前,我们需要了解一下它们的区别:

特性setTimeoutsetInterval

执行次数

只执行一次

可以无限次执行,直到被清除

精确度

更加精确,因为它只执行一次

如果回调函数执行时间过长,可能会累积延迟

回调执行控制

需要手动再次调用 setTimeout 来实现重复执行

自动重复执行,无需额外代码

适用场景

单次延迟后的操作

需要周期性执行的任务

手写实现cunstomInterval:

我们在了解区别后,就可以来列出我们的需求:

而接下来就是代码实现了

function customSetInterval(fn, time) {

    let intervalId = null;

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

    loop(); // 启动首次循环

    return () => clearTimeout(intervalId);

而这里发生了以下几件事情:

立即返回一个函数:当 customSetInterval 被调用时,它不仅启动了定时器循环,还返回了一个新的函数。这个新函数并不立即执行,而是作为 customSetInterval 的返回值传递给调用者。保持对 intervalId 的引用:由于 JavaScript 中的闭包特性,返回的匿名函数能够记住并访问 customSetInterval 内部的 intervalId 变量,即使 customSetInterval 函数本身已经执行完毕。这意味着即使在 customSetInterval 执行完成后,返回的函数仍然可以访问和操作 intervalId。调用 clearTimeout:当外部代码最终调用这个返回的函数时,它会执行内部的 clearTimeout(intervalId),从而取消与 intervalId 关联的定时器。这将阻止后续的 setTimeout 回调被触发,有效地终止了循环。示例:

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) {
            let intervalId = null;
            function loop() {
                intervalId = setTimeout(() => {
                    fn();
                    loop();
                },time)
            }
            loop();
            return () => clearTimeout(intervalId);
        }
        const interval = customSetInterval(function() {
            console.log('这就是黄金体验镇魂曲')
        },1000)
        setTimeout(() => {
            interval();
        },5000)
    script>
body>
html>

image.png

深入思考:为什么有时需要使用 setTimeout 替代 setInterval1. 避免回调堆积

setInterval 在设定的时间间隔后会尝试立即执行回调函数,而不考虑前一次回调是否已经完成。如果回调函数的执行时间超过了间隔时间,或者由于事件循环繁忙导致延迟,可能会出现多个回调排队等待执行的情况,这被称为“回调堆积”。

使用 setTimeout 可以确保每次回调完成后才设置下一次执行的时间点,从而避免了回调堆积的问题。通过递归调用 setTimeout,可以精确控制每次执行之间的间隔,并确保不会累积未处理的任务。例如:

function customInterval(callback, delay) {
    function loop() {
        setTimeout(() => {
            callback();
            loop(); // 递归调用以维持间隔
        }, delay);
    }
    loop();
}

2. 灵活的任务管理

使用 setTimeout 模拟 setInterval 允许我们在每次回调中决定是否继续执行后续的定时器。例如,我们可以在特定条件下终止重复执行,或者动态调整下次执行的时间间隔。

let shouldContinue = true;
function flexibleInterval(callback, time) {
    function loop() {
        if (shouldContinue) {
            setTimeout(() => {
                callback();
                loop();
            }, time);
        }
    }
    loop();
    return () => { shouldContinue = false; };
}
// 示例
const stopFlexibleInterval = flexibleInterval(() => {
    console.log('XXX');
}, 1000);
// 动态停止
stopFlexibleInterval();

3. 处理长时间运行的任务

当回调函数包含长时间运行的任务时,使用 setInterval 可能会导致间隔时间不准确,甚至引起浏览器卡顿。而使用 setTimeout 可以更好地适应这种情况,因为它只会在上一个任务完成后才安排下一个任务。

小结:

通过深入学习 setTimeout,我们不仅掌握了如何精确控制异步任务的延迟执行,还学会了利用它来构建更灵活、可控的定时器机制,如模拟 setInterval,从而提升了处理复杂定时任务的能力。


上一条查看详情 +智能图像识别系统设计与实现
下一条 查看详情 +没有了