- 作者:老汪软件技巧
- 发表时间: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实现在等待一秒后打印"----",我们来看看输出结果。
但是当我们在中间添加一个死循环会发生什么?定时器会在到达指定时间后执行吗?
// 在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)
在执行后我们就会发现,仍然是之前的结果
这是因为主线程的执行时间可能会超过指定时间,而由于其在主线程执行完后才会执行的特性,所以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>
在上述代码中,我们创建了两个按钮用于管理定时器,在未关闭定时器时会执行
而当我们在10s前点击”提前关闭setTimeout定时器“,那么就不会打印”----“。而也可以通过点击”关闭定时器“来结束setInterval的任务。
在setInterval执行11次后(即运行了11s)也没有进行打印"----",说明setTimeout已经结束了,而在点击”关闭定时器“后,setInterval也不再执行。
一句话总结setTimeout
在了解了上述setTimeout的性质后,我们就可以向面试官解释什么是setTimeout:
setTimeout 是 JavaScript 的异步计时器,它在接受一个回调函数和延迟时间(毫秒)作为参数后,将该回调加入事件循环队列,会在主线程执行完后才会执行,实际执行时间可能因主线程的繁忙程度而晚于指定时间,可以通过setTimeout 返回的定时器ID可取消执行。
用 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>
深入思考:为什么有时需要使用 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,从而提升了处理复杂定时任务的能力。