之前在写vue处理事件延迟执行,一般会用到setTimeout,而很少关注到setInterval 当然面试时候,如果简单问一下setTimeout可能会答,但是问到和setInterval联系和区别可能不得而知。下文就来聊聊定时器 setTimeout和setInterval 时延问题
引入
下面代码中,设置定时器time为0,会立即执行吗?
<script>
setTimeout(function(){
console.log("hello world");
},0)
console.log("先执行");
</script>
答案是不会,那为什么?
实际上,即使我们将 setTimeout 的延迟时间设置为 0 毫秒,回调函数也不会立刻执行。这是因为 JavaScript 是单线程语言,所有的同步任务都会在一个叫做 调用栈 的结构中依次执行,直到调用栈为空,才会去检查 事件队列 中是否有待处理的任务
当 setTimeout 的时间到达(即使是 0 毫秒),它的回调函数会被放入 宏任务队列 中,而不是直接加入到调用栈中执行。只有当当前的同步代码完全执行完毕,即调用栈清空后,JavaScript 引擎才会从宏任务队列中取出第一个任务并执行。因此,在上面的例子中,console.log("先执行") 会先于 console.log("hello world") 被打印出来,因为它是同步代码的一部分,而 setTimeout 的回调则属于异步任务。
如果你不了解事件队列这方面的,这里有面试题有关事件处理和Promise详细解析
setTimeout VS setInterval
让我们先来区分一下 setTimeout 和 setInterval 这两个定时器函数的作用:
setTimeout:这是一个一次性定时器,它会在指定的延迟时间之后执行一次给定的回调函数。如果需要停止定时器,可以使用clearTimeout函数,并传入由setTimeout返回的定时器 ID。例如,你可以提供一个按钮让用户点击以终止定时器的执行。setInterval:这是一个循环定时器,它会按照设定的时间间隔重复执行同一个回调函数,直到你显式地调用clearInterval来停止它。需要注意的是,setInterval可能会在前一个回调还未完成时就再次触发新的回调,这可能会导致一些意想不到的行为,比如多个回调堆积在一起快速执行
看下面代码进行加深理解:
<button id="btn">提前关闭setTimeout定时器</button>
<button id="btn1">关闭setInterval循环定时器</button>
<script>
const btn = document.getElementById('btn');
btn.addEventListener('click',()=>{
clearTimeout(settimeout)
})
const settimeout = setTimeout(()=>{
console.log('我是一次性定时器')
},4000)
const btn1 = document.getElementById('btn1');
btn1.addEventListener('click',()=>{
clearInterval(interval)
})
const interval = setInterval(()=>{
console.log('我是循环定时器')
},1000)
</script>
4s后“一次执行事件”结束。“循环”事件续上。只点击“关闭setInterval”事件,“循环事件”不再执行。
点击“提前关闭setTimeout定时器”,4s后不执行“setTimeout”事件
使用 setTimeout 模拟 setInterval 的实现方法
有时我们希望规避 setInterval 的一些潜在缺点,例如它不会等待前一个回调函数执行完毕再启动下一个,而是严格根据设定的时间间隔触发,这可能导致回调函数的累积调用,尤其是在回调函数执行时间超过设定间隔时。
为了解决这一问题,我们可以利用 setTimeout 通过递归的方式来模拟 setInterval 的行为,确保每次回调都是在上一次回调完成后才开始计时。
<script>
/**
* 使用 setTimeout 实现类似 setInterval 的功能
* @param {Function} fn - 要执行的回调函数
* @param {Number} time - 两次回调之间的间隔时间(毫秒)
* @returns {Function} - 用于清除定时器的函数
*/
function customSetInterval(fn, time) {
let timerId;
function loop() { // 定义递归调用的内部函数
timerId = setTimeout(() => {
try {
fn(); // 执行传入的回调函数
} catch (error) {
console.error('回调函数执行出错:', error);
}
loop(); // 递归调用以保持循环
}, time);
}
loop(); // 启动第一次调用
return () => { // 返回一个函数,用于停止定时器
clearTimeout(timerId);
console.log('定时器已关闭');
};
}
const interval = customSetInterval(() => { // 创建一个自定义的循环定时器
console.log("Hello, world!");
}, 1000);
// 在5秒后关闭定时器
setTimeout(() => {
interval();
}, 5000);
</script>
这种方法不仅能够保证每个回调都在前一个回调完成后才开始计时,而且还允许我们在必要时优雅地结束定时器,避免了 setInterval 可能带来的累积问题。
优雅结束~~