前言
在 JavaScript 异步编程中,定时器是最基础的工具。但很多开发者只知道它们能延迟执行,却忽略了它们的误差来源、最大值限制以及浏览器后台运行优化等底层细节。本文将带你全面梳理这两个核心 API。## 一、 setTimeout:延时执行的艺术
1. 基础概念
setTimeout 用于在指定的毫秒数后调用函数。
语法:
let timeoutId = setTimeout(callback, delay);
- 单次执行:回调函数只会执行一次。
- 返回值:返回一个正整数 ID,可通过
clearTimeout(id)取消执行。
2. 你必须知道的“潜规则”
- 执行时间误差:由于它是宏任务,必须等待同步代码和微任务队列清空后才会执行。如果主线程被阻塞,实际执行时间会远超预设时间。
- 最大值限制:浏览器以 32 位整型存储延迟时间。这意味着最大值为 毫秒(约 24.8 天)。如果超过这个值,延迟会被重置为 0,导致立即执行。
- 非活动标签页限制:为了节省电量和 CPU,浏览器会对未激活的后台标签页进行优化,定时器的最小间隔通常会被强制限制在 1000ms。
二、 setInterval:循环执行的利弊
1. 基础概念
setInterval 每隔设定的时间固定调用回调函数,直到被手动清除。
语法:
let intervalId = setInterval(callback, delay);
2. 核心演示
let i = 1;
const timerId = setInterval(() => {
console.log(i++);
if (i > 5) {
clearInterval(timerId); // 到达条件后必须清除,否则会造成内存泄漏
}
}, 1000);
三、 深度对比:两者有什么区别?
| 特性 | setTimeout | setInterval |
|---|---|---|
| 执行次数 | 仅一次 | 循环往复,直到清除 |
| 精准度 | 存在误差(受主线程阻塞影响) | 存在误差(可能发生“丢帧”或堆积) |
| 应用场景 | 延迟加载、防抖、单次异步 | 轮询、实时进度条、时钟显示 |
进阶对比:链式 setTimeout vs setInterval
面试中常问:为什么推荐用“链式 setTimeout”来模拟循环,而不是直接用 setInterval?
-
setInterval的问题:它不关心回调函数的执行耗时。如果回调函数执行时间长于间隔时间,两次执行之间会没有任何间隙,甚至发生任务堆积。 -
链式
setTimeout的优势:function repeat() { // 逻辑代码... setTimeout(repeat, 1000); // 在代码执行完后再设置下一次定时 }这样可以确保两次执行之间的间隙始终是 1000ms,不会发生重叠。
四、 面试模拟题
Q1:为什么 setTimeout(fn, 0) 不会立即执行?
参考回答:
- 事件循环机制:即使延迟是 0,
fn也会被放入宏任务队列。主线程必须先执行完同步代码和微任务队列。 - 最小间隔:HTML5 规范规定,如果定时器嵌套层级超过 5 层,最小延迟会被强制设为 4ms。
Q2:如何实现一个准时的定时器?
参考回答:
由于 JS 单线程的特性,无法做到绝对准时。但可以通过以下方式优化:
- RequestAnimationFrame:如果是 UI 动画,使用
rAF随屏幕刷新率执行。 - 计算偏移量:在每次执行时,通过
Date.now()获取当前时间,计算与目标时间的差值,动态调整下一次setTimeout的延迟时间。
Q3:如果设置 setInterval(fn, 100),而 fn 执行需要 200ms,会发生什么?
参考回答:
JS 引擎会发现上一个任务还在执行,它会将新的任务放入队列。但为了避免无限堆积,部分浏览器会优化逻辑,如果队列中已有该定时器的回调,则不再添加。这会导致原本应该执行多次的任务看起来像是“连在一起执行”或者“跳过了某些执行”。
五、 总结建议
- 清理定时器:无论是单次还是循环,在 React/Vue 组件销毁时,务必调用
clear方法,防止内存泄漏。 - 首选 setTimeout:对于复杂的循环逻辑,链式
setTimeout比setInterval更受控且安全。