在 14.5 节中介绍了 setTimeout、setInterval 和 requestAnimationFrame 这三个定时器的基础用法,但要想在实际工程中避坑,必须深入理解它们的执行时机、精度限制以及各自最适合的场景。
执行时机:回调何时真正运行?
定时器的回调并不是在设定的时间点准时执行,而是在 ≥ 设定时间后的某个时刻被执行。这背后是事件循环在协调:
setTimeout(fn, delay):计时器线程在delay毫秒后将fn推入宏任务队列。- 当调用栈清空(同步代码执行完毕),事件循环开始从宏任务队列中取出任务。
- 如果此时主线程正忙(比如在执行一个长循环,或者先前的宏任务还未结束),
fn就得继续排队等待,导致实际执行延迟 >delay。
setInterval 类似,但还有一个额外的风险:如果回调执行时间过长,可能在下一次触发时前一次还没执行完,浏览器会将后续的回调直接丢弃或排队堆积(取决于环境实现),容易出现间隔不稳定或骤发执行。
requestAnimationFrame(callback) 的执行时机完全不同:它由浏览器渲染引擎驱动,在每一帧绘制之前统一调用。通常这对应每秒约 60 帧(16.7ms 一帧),在显示刷新率更高的设备上可能达到 90 帧或 120 帧。当页面切换到后台或不可见时,requestAnimationFrame 自动暂停,降低 CPU 和电量消耗。
精度问题:为什么实际延迟总比设的长?
setTimeout / setInterval 的精度短板:
- 最小延迟限制:HTML 标准规定,嵌套超过 5 层的
setTimeout/setInterval,最小延迟强制为 4ms。对setInterval,即便设置delay = 0,实际间隔也可能被抬高到 4ms。 - 后台标签页节流:现代浏览器对不可见标签页的定时器频率严格限制,最小延迟通常被抬高到 1000ms(1秒),以节省资源。
- 系统时钟漂移:定时器依赖系统时钟,如果用户调整系统时间,可能造成意外触发或延迟。
- 调用栈阻塞:主线程上的长时间同步计算会直接推迟所有宏任务的执行,让定时器“迟到”。
综合来看,setTimeout 和 setInterval 的延迟只能承诺“不早于”,不能承诺“准时于”。
requestAnimationFrame 的高精度:
它与屏幕刷新率严格同步,每一帧之间的间隔相对稳定(如 16.7ms)。在播放动画时,它能完美避免因定时器精度不足导致的画面撕裂或掉帧,浏览器还可以在合成线程层面对其进行优化。换言之,requestAnimationFrame 是与视觉呈现绑定的“心搏”,不适用于需要精确计时但无关视觉的场景。
使用场景与最佳实践
setTimeout
- 单次延时任务:延迟显示提示、一段时间后自动关闭弹窗、防抖(debounce)等。
- 将一个高开销任务拆分成多个小片段,利用
setTimeout(fn, 0)让出主线程,防止 UI 卡顿(时间切片)。 - 递归
setTimeout替代setInterval实现稳定定时任务:因为每一轮都是在上一次回调执行完后再设置新的定时器,避免回调堆积。
// 递归 setTimeout 模拟稳定间隔
function safeInterval(fn, delay) {
let timer;
function loop() {
fn();
timer = setTimeout(loop, delay);
}
timer = setTimeout(loop, delay);
return () => clearTimeout(timer);
}
setInterval
少用!仅当间隔极短(如小于 10ms?别想了有最小延迟限制)且回调执行时间稳定可预测时才考虑使用。否则,累积误差和回调堆积会让你难以掌控。大多数时候,递归 setTimeout 是更安全的“定时循环”方案。
requestAnimationFrame
- 动画(DOM 动画、Canvas 动画、WebGL 场景刷新),确保每一帧都在渲染前更新状态,获得流畅的视觉体验。
- 监听屏幕滚动、元素尺寸变化(配合
getBoundingClientRect做 sticky 效果或滚动动画),避免高频触发导致的重排。 - 大批量操作 DOM 时,可以将操作分散到每一帧执行,不给用户造成卡顿。
let start;
function animate(timestamp) {
if (!start) start = timestamp;
const elapsed = timestamp - start;
// 根据 elapsed 更新动画状态
if (elapsed < 2000) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
归根结底:需要准确时间间隔的用 setTimeout 并接受其精度限制;需要与渲染同步的务必用 requestAnimationFrame;而 setInterval 则保留在框架或游戏引擎内部,普通业务代码请谨慎绕过。