人人都会AI编程

14.5 定时器:setTimeout、setInterval、requestAnimationFrame

更新时间:2026-07-11

在 14.5 节中介绍了 setTimeoutsetIntervalrequestAnimationFrame 这三个定时器的基础用法,但要想在实际工程中避坑,必须深入理解它们的执行时机、精度限制以及各自最适合的场景。

执行时机:回调何时真正运行?

定时器的回调并不是在设定的时间点准时执行,而是在 ≥ 设定时间后的某个时刻被执行。这背后是事件循环在协调:

  1. setTimeout(fn, delay):计时器线程在 delay 毫秒后将 fn 推入宏任务队列
  2. 当调用栈清空(同步代码执行完毕),事件循环开始从宏任务队列中取出任务。
  3. 如果此时主线程正忙(比如在执行一个长循环,或者先前的宏任务还未结束),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秒),以节省资源。
  • 系统时钟漂移:定时器依赖系统时钟,如果用户调整系统时间,可能造成意外触发或延迟。
  • 调用栈阻塞:主线程上的长时间同步计算会直接推迟所有宏任务的执行,让定时器“迟到”。

综合来看,setTimeoutsetInterval 的延迟只能承诺“不早于”,不能承诺“准时于”

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 则保留在框架或游戏引擎内部,普通业务代码请谨慎绕过。