定时器是 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 毫秒后一定会执行。实际执行时间取决于两个因素:
- 主线程是否空闲:如果主线程正在执行耗时任务,回调必须等待。
- 嵌套层级的延迟下限:当
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 毫秒恰好执行一次,实际并非如此:
- 回调可能被跳过:如果某次回调执行时间超过了间隔时间,下一次回调会直接被跳过(而不是排队等待)。
- 累积问题:即使回调执行时间很短,微小的延迟也会日积月累,导致“漂移”。
// 模拟一个执行时间不稳定的回调
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
- 与屏幕刷新同步,避免无意义的计算
如果屏幕是 60Hz,一帧只有约 16.7ms。用 setTimeout(fn, 16) 做动画,会因为时间不精确导致:
- 某些帧被跳过(画面卡顿)
- 某些帧在同一周期内执行两次(浪费计算,用户看不到)
- 自动节流,节省资源
当用户切换到其他标签页或最小化浏览器时,requestAnimationFrame 自动暂停,不给用户增加无谓的 CPU 和电池消耗。而 setInterval 会继续占用资源。
- 多个动画自动汇集到同一帧
页面上如果有多个动画(如侧边栏收起、图片淡入、滚动平滑),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 使用定时器时的注意事项
- 及时清理:组件销毁或页面跳转前,务必调用
clearTimeout/clearInterval/cancelAnimationFrame,否则会造成内存泄漏和意外行为。 - 不要依赖精确时间:所有定时器的执行时间都不精确,关键逻辑需要记录实际时间戳来判断。
- 避免长时间
setInterval:如需高频循环,优先使用requestAnimationFrame或嵌套setTimeout。 - 后台标签页的行为:浏览器会对后台标签页中的
setTimeout和setInterval降低频率(通常降到 1 秒一次),不要在后台标签页中依赖高频定时器。
理解了这三个定时器的工作原理和差异,你就能在正确的场景选择正确的工具,避免时间相关的 bug,写出流畅的用户体验。