引言
由于js是一种单线程的且只有一个主线程的编程语言,这就意味着它在同一时间只能执行一个任务。为了让我们可以实现异步操作,js提供了setTimeout和setInterval两个内置函数来实现延时执行和周期性执行代码片段的能力。本文将深入探讨这两种的工作原理并讲解如何用setTimeout来手写一个setInterval。
setTimeout
基本用法
setTimeout 是 JavaScript 中用于设置延时执行的函数。它允许你指定一段代码(通常是一个函数),并在给定的时间延迟后执行这段代码,它的基本用法如下:
const timeoutId = setTimeout(callback, delay);
- callback:一个函数或可调用对象,将在指定时间后被调用。
- delay:一个整数,表示多少毫秒(ms)后执行回调函数。
来看一段代码:
setTimeout(function(){
console.log("-------");
},1000)
console.log(123);
这段代码结果也正如我们所讲,先执行了123,在执行 ------- 。
异步调用
setTimeout 并不会阻塞主线程的执行,它会将回调函数放入事件队列中等待执行。这是因为 JavaScript 是单线程的,所有任务(包括同步代码和异步回调)都在同一个线程上顺序执行。请看下面这段代码:
setTimeout(function(){
console.log("-------");
},0)
console.log(123);
我们在这里把延迟时间设置为0了,但打印以后仍然是上面那张图,只不过上面的要等1s才会打印 ------- ,而现在几乎是立马打印的。
取消定时器
setTimeout的返回值是一个定时器 ID (timeoutId),可以用来取消这个定时器(通过 clearTimeout(timeoutId))。如果在计时结束前调用了 clearTimeout,那么回调函数将不会被执行。请看下面这段代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>setTimeout Example</title>
</head>
<body>
<button id="stopTimeout">提前关闭setTimeout定时器</button>
<script>
const stopButton = document.getElementById('stopTimeout');
let timeoutId;
// 设置一个10秒后的定时器
timeoutId = setTimeout(() => {
console.log('10秒后打印此消息');
}, 10000);
// 添加点击事件监听器来提前取消定时器
stopButton.addEventListener('click', function() {
clearTimeout(timeoutId);
console.log('定时器已提前取消');
});
</script>
</body>
</html>
它的运行结果如下图,当我们点击了提前关闭定时器后,不论等多久它都不会在控制台中打印我们想打印的东西。
setInterval
setInterval 与 setTimeout 类似,但它不是只执行一次回调函数,而是按照指定的时间间隔重复执行,直到使用 clearInterval(intervalId) 明确停止为止。其基本语法是:
const intervalId = setInterval(callback, delay);
- callback:一个函数或可调用对象,将以固定的间隔时间被调用。
- delay:一个整数,表示每次回调之间的等待时间(以毫秒为单位)。
同样地,setInterval 返回一个定时器 ID (intervalId),可用于停止定时器。
取消定时器
下面是setInterval如何取消定时器的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>setInterval Example</title>
</head>
<body>
<button id="startInterval">开始setInterval定时器</button>
<button id="stopInterval">关闭setInterval定时器</button>
<script>
const startButton = document.getElementById('startInterval');
const stopButton = document.getElementById('stopInterval');
let intervalId;
// 定义要周期性执行的函数
function periodicFunction() {
console.log('每秒打印一次');
}
// 开始定时器
startButton.addEventListener('click', function() {
intervalId = setInterval(periodicFunction, 1000);
console.log('定时器已启动');
});
// 停止定时器
stopButton.addEventListener('click', function() {
clearInterval(intervalId);
console.log('定时器已停止');
});
</script>
</body>
</html>
如何用 setTimeout 手写 setInterval
最近听说自己的一个学长在大厂面试中就挂在这道题目上了,笔者正好写一下来熟悉一下,想法应该很简单,我们可以用递归来实现。下面随笔者一起来看看如何实现吧。
1. 定义 customSetInterval 函数
-
参数:
fn:要周期性执行的函数。time:每次执行之间的间隔时间(以毫秒为单位)。
-
内部变量和方法:
intervalId:存储当前定时器的 ID,用于后续清除定时器。loop():这是一个递归调用自身的辅助函数,它负责设置下一个setTimeout并在每次计时结束时调用传入的函数fn。
2. 初始化循环
loop()函数被立即调用一次,开始第一次延迟后执行fn的过程。这一步是至关重要的,因为它启动了整个周期性的执行流程。
3. 返回一个清除定时器的方法
customSetInterval返回一个匿名函数,这个函数的作用是清除定时器 (clearInterval(intervalId))。这样做是为了提供一种机制来停止周期性任务。
4. 使用 customSetInterval
- 创建了一个名为
interval的变量,它保存了对customSetInterval返回的清除函数的引用。这意味着你可以通过调用interval()来停止周期性的执行。
5. 停止定时器
- 使用
setTimeout设置了一个额外的定时器,在 5 秒后调用interval()方法来停止由customSetInterval创建的周期性任务。
以下是关键步骤:
-
定义一个外部函数:这个函数接受两个参数——一个是要定期执行的回调函数
fn,另一个是每次执行之间的等待时间time。 -
创建一个递归调用的辅助函数:这个辅助函数(如代码中的
loop)会在每次调用时重新设置一个新的setTimeout,并在延迟结束后再次调用自己,从而形成一个连续的执行链。 -
初始化第一次延迟:通过立即调用辅助函数来启动第一个延迟计时。
-
提供停止定时器的能力:返回一个可以用来清除定时器的函数,以便可以在任何时候停止周期性执行。
-
管理定时器 ID:确保你有办法追踪每个定时器的 ID,这样可以在需要的时候正确地清除它们。
注意事项
-
防止堆栈溢出:由于
loop是递归调用的,理论上可能会导致 JavaScript 的调用栈溢出。然而,因为每次递归调用都是通过setTimeout触发的,而不是直接调用,所以实际上不会发生这种情况。每次递归调用都会在事件循环的下一轮中进行,因此不会累积在调用栈中。 -
保证异步非阻塞:这种方式确保了即使回调函数
fn的执行时间超过了设定的时间间隔,也不会影响其他操作或导致浏览器卡顿,因为所有的回调都是异步执行的。
具体代码如下:
function customSetInterval(fn, time) {
let intervalId = null;
function loop() {
intervalId = setTimeout(() => {
fn();
loop();
}, time)
}
loop()
return () => clearInterval(intervalId)
}
const interval= customSetInterval(function () {
console.log('qa');
}, 1000)
setTimeout(() => {
interval()
}, 5000)
结果如下图: