人人都会AI编程

14.5 定时器:setTimeout、setInterval、requestAnimationFrame

更新时间:2026-07-11

定时器是 JavaScript 中处理时间相关操作的核心工具。浏览器提供了三种主要的定时器 API,它们各有侧重,理解它们的区别和适用场景,是写出流畅、高效前端代码的基本功。

14.5.1 setTimeout:延迟执行一次

setTimeout 用于在指定的延迟时间后,执行一次回调函数。

基本语法

const timerId = setTimeout(callback, delay, arg1, arg2, ...);
  • callback:延迟结束后要执行的函数
  • delay:延迟时间,单位毫秒(默认值为 0)
  • arg1, arg2, ...:传给 callback 的参数
  • 返回值 timerId:定时器的唯一标识,用于取消定时

实际用法

// 2 秒后打印一条消息
setTimeout(() => {
  console.log('2 秒到了');
}, 2000);

// 带参数的写法
setTimeout((name, age) => {
  console.log(`${name} 今年 ${age} 岁`);
}, 1000, '张三', 25);

// 配合取消定时
const timerId = setTimeout(() => {
  console.log('这条消息永远不会出现');
}, 5000);

clearTimeout(timerId); // 立即取消,回调不会执行

重要认知:延迟时间是最小值,不是精确值

setTimeout(fn, 1000) 并不意味着 1000 毫秒后一定会执行。实际执行时间取决于两个因素:

  1. 主线程是否空闲:如果主线程正在执行耗时任务,回调必须等待。
  2. 嵌套层级的延迟下限:当 setTimeout 嵌套超过 5 层时,浏览器会将最小延迟强制设为 4 毫秒。
console.time('test');

setTimeout(() => {
  console.timeEnd('test'); // 几乎不可能恰好显示 1000ms
}, 1000);

// 紧接着写一个耗时循环,让主线程忙起来
for (let i = 0; i < 1000000000; i++) {
  // 空循环,大约占用几百毫秒
}
// 上面的循环完成后,setTimeout 的回调才开始执行
// 实际延迟 = 1000ms + 循环耗时

零延迟的真相

setTimeout(() => {
  console.log('我是第二');
}, 0);

console.log('我是第一');
// 输出顺序:我是第一 → 我是第二

即使延迟设为 0,回调也不会立即执行,而是等到当前同步代码执行完后才进入事件循环。这是理解 JavaScript 异步行为的关键示例。

14.5.2 setInterval:周期性重复执行

setInterval 用于每隔指定的时间间隔,重复执行回调函数。

基本语法

const timerId = setInterval(callback, interval, arg1, arg2, ...);

实际用法

// 每秒打印一次当前时间
const timerId = setInterval(() => {
  console.log(new Date().toLocaleTimeString());
}, 1000);

// 10 秒后停止
setTimeout(() => {
  clearInterval(timerId);
  console.log('定时器已停止');
}, 10000);

setInterval 的隐藏问题

很多人误以为 setInterval(fn, 1000) 能保证每隔 1000 毫秒恰好执行一次,实际并非如此:

  1. 回调可能被跳过:如果某次回调执行时间超过了间隔时间,下一次回调会直接被跳过(而不是排队等待)。
  2. 累积问题:即使回调执行时间很短,微小的延迟也会日积月累,导致“漂移”。
// 模拟一个执行时间不稳定的回调
setInterval(() => {
  const start = Date.now();
  while (Date.now() - start < 300) {
    // 模拟耗时 300ms 的操作
  }
  console.log('执行完成');
}, 200);
// 间隔 200ms,但每次执行要 300ms
// 浏览器会不断跳帧,极不稳定

更推荐的替代方案:嵌套 setTimeout

function repeatWithSetTimeout() {
  console.log('执行任务');
  
  setTimeout(repeatWithSetTimeout, 1000);
  // 在上一次回调执行完成后,再等 1 秒
  // 两次执行的间隔永远是稳定的 1000ms + 任务耗时
}

repeatWithSetTimeout();

嵌套 setTimeout 的优点是:下一次计时从上一次执行结束开始算起,不会出现回调堆积或跳过的问题。对于需要稳定间隔的循环任务(如轮询服务器),这是更健壮的方案。

14.5.3 requestAnimationFrame:与屏幕刷新同步

requestAnimationFrame 是专门为动画设计的定时器,它的执行时机与浏览器的屏幕刷新率同步。

基本语法

const requestId = requestAnimationFrame(callback);

// 取消
cancelAnimationFrame(requestId);

与 setTimeout/setInterval 的本质区别

| 特性 | setTimeout / setInterval | requestAnimationFrame |
|------|--------------------------|------------------------|
| 触发时机 | 基于固定时间间隔 | 跟随屏幕刷新率(通常 60fps,约每 16.7ms 一次) |
| 后台标签页行为 | 继续执行(频率降低至 1 秒一次) | 自动暂停,切回后恢复 |
| 适用场景 | 延时操作、轮询、非视觉任务 | 动画、滚动、视觉更新 |
| 性能影响 | 可能造成掉帧或过度绘制 | 自动优化,不会在不可见时浪费资源 |

为什么动画必须用 requestAnimationFrame

  1. 与屏幕刷新同步,避免无意义的计算

如果屏幕是 60Hz,一帧只有约 16.7ms。用 setTimeout(fn, 16) 做动画,会因为时间不精确导致:

  • 某些帧被跳过(画面卡顿)
  • 某些帧在同一周期内执行两次(浪费计算,用户看不到)
  1. 自动节流,节省资源

当用户切换到其他标签页或最小化浏览器时,requestAnimationFrame 自动暂停,不给用户增加无谓的 CPU 和电池消耗。而 setInterval 会继续占用资源。

  1. 多个动画自动汇集到同一帧

页面上如果有多个动画(如侧边栏收起、图片淡入、滚动平滑),requestAnimationFrame 会将它们全部排到同一帧内执行,避免多次重排重绘。

实际用法:平滑动画

// 纯 JavaScript 动画示例:让一个方块平滑移动
const box = document.getElementById('box');
let position = 0;
const targetPosition = 500;
const duration = 1000; // 1 秒
const startTime = performance.now();

function animate(currentTime) {
  const elapsed = currentTime - startTime;
  const progress = Math.min(elapsed / duration, 1); // 0 到 1
  
  // 使用缓动函数让动画更自然(easeInOutQuad)
  const easeProgress = progress < 0.5
    ? 2 * progress * progress
    : 1 - Math.pow(-2 * progress + 2, 2) / 2;
  
  position = easeProgress * targetPosition;
  box.style.transform = `translateX(${position}px)`;
  
  if (progress < 1) {
    requestAnimationFrame(animate); // 继续下一帧
  }
}

// 启动动画
requestAnimationFrame(animate);

这个例子展示了 requestAnimationFrame 的标准用法:每一帧根据时间流逝计算动画进度,更新元素位置,当动画未完成时递归调用自身。callback 默认接收一个时间戳参数(DOMHighResTimeStamp),可以精确计算已用时间。

14.5.4 三者的选型指南

| 场景 | 推荐方案 | 原因 |
|------|---------|------|
| 延时跳转、提示消失 | setTimeout | 只需执行一次 |
| 轮询服务器状态 | 嵌套 setTimeout | 避免回调堆积,间隔稳定 |
| 简单的秒表、倒计时 | setInterval | 实现简单,偏差可接受 |
| 复杂动画、平滑滚动 | requestAnimationFrame | 与刷新率同步,流畅且节能 |
| 高频率的后台任务(如日志上报) | setTimeout + 节流 | 避免频繁操作影响性能 |

14.5.5 使用定时器时的注意事项

  1. 及时清理:组件销毁或页面跳转前,务必调用 clearTimeout / clearInterval / cancelAnimationFrame,否则会造成内存泄漏和意外行为。
  2. 不要依赖精确时间:所有定时器的执行时间都不精确,关键逻辑需要记录实际时间戳来判断。
  3. 避免长时间 setInterval:如需高频循环,优先使用 requestAnimationFrame 或嵌套 setTimeout
  4. 后台标签页的行为:浏览器会对后台标签页中的 setTimeoutsetInterval 降低频率(通常降到 1 秒一次),不要在后台标签页中依赖高频定时器。

理解了这三个定时器的工作原理和差异,你就能在正确的场景选择正确的工具,避免时间相关的 bug,写出流畅的用户体验。