4、JavaScript定时器动画:setTimeout与setInterval的区别与使用,requestAnimationFrame的引入及其优势
说到JavaScript动画,很多人第一反应就是CSS3的transition和animation。但说实话,真正复杂的动画场景,还是得靠JS来掌控。今天我们就来聊聊JS定时器动画,以及为什么后来大家都转向了requestAnimationFrame。
4.1 setTimeout与setInterval:老牌定时器
这两个API,我相信大家都用过。但你真的了解它们的区别吗?
4.1.1 setTimeout:一次性定时器
setTimeout的意思很简单——等一段时间,执行一次。要实现动画效果,就得递归调用自己。
// 一个简单的数字跳动
let count = 0;
function tick() {
count++;
document.getElementById('clock').textContent = count;
setTimeout(tick, 1000); // 每秒调用一次
}
tick();
嗯,这里要注意。setTimeout的延迟时间并不是精确的。它只是说「至少等这么久」,而不是「刚好等这么久」。我在项目中遇到过一个问题:用setTimeout做倒计时,结果跑着跑着就慢了。为什么?因为setTimeout的回调执行本身也需要时间,累积下来误差就大了。
4.1.2 setInterval:重复定时器
setInterval会每隔一段时间执行一次,不需要手动递归。
let count = 0;
setInterval(() => {
count++;
document.getElementById('clock').textContent = count;
}, 1000);
看起来比setTimeout简洁,对吧?但坑也更多。
4.1.3 两者的核心区别
| 特性 | setTimeout | setInterval |
|---|---|---|
| 执行方式 | 延迟后执行一次 | 每隔固定时间执行一次 |
| 误差累积 | 有,但可手动修正 | 有,且难以修正 |
| 回调堆积 | 不会 | 会(回调执行超时时) |
| 推荐场景 | 需要精确控制间隔的动画 | 简单重复任务 |
我个人习惯,做动画时优先用setTimeout递归。虽然代码多写几行,但可控性更强。setInterval更适合那些「丢了就丢了」的定时任务,比如轮询状态。
4.2 requestAnimationFrame:动画的救星
你想想看,setTimeout和setInterval都是基于「时间」的。但浏览器渲染有自己的节奏——通常是60帧/秒,也就是每16.7ms渲染一次。如果你用setTimeout每隔10ms更新一次DOM,浏览器可能根本来不及渲染,白白浪费性能。
requestAnimationFrame(简称rAF)就是来解决这个问题的。它会告诉浏览器:「嘿,下次你渲染之前,记得叫我一声。」
4.2.1 rAF的优势
- 与浏览器刷新同步:rAF的回调会在每次浏览器重绘之前执行,保证动画流畅
- 节省资源:页面不可见时,rAF会自动暂停,不会浪费CPU
- 更精确的时间戳:回调参数中会传入一个高精度时间戳,方便做时间计算
4.2.2 用rAF实现数字跳动时钟
好了,理论说完了,我们来写一个真正的数字跳动时钟。这次我们用rAF来实现,并且加入毫秒显示,让跳动更明显。
// 获取显示元素
const hoursEl = document.getElementById('hours');
const minutesEl = document.getElementById('minutes');
const secondsEl = document.getElementById('seconds');
const msEl = document.getElementById('milliseconds');
let lastTime = 0;
function updateClock(timestamp) {
// timestamp是rAF传入的高精度时间戳
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const ms = String(now.getMilliseconds()).padStart(3, '0');
hoursEl.textContent = hours;
minutesEl.textContent = minutes;
secondsEl.textContent = seconds;
msEl.textContent = ms;
// 继续下一帧
requestAnimationFrame(updateClock);
}
// 启动动画
requestAnimationFrame(updateClock);
这段代码看起来很简单,但效果很丝滑。毫秒数字会流畅地跳动,而不是像setTimeout那样一顿一顿的。
if (timestamp - lastTime < 33.3) return; // 跳过这一帧这样既保留了rAF的优势,又能控制更新频率。
4.3 避坑指南
做定时器动画,有几个坑我踩过,分享给你:
- 不要用setInterval做精确计时——误差会累积,跑久了就不准了。我曾经做过一个在线考试系统,用setInterval做倒计时,结果考试结束时间比实际晚了将近1分钟,被考生投诉了。
- rAF的回调参数timestamp不要自己算——直接用rAF传进来的时间戳,它比Date.now()更精确,而且与浏览器同步。
- 记得清理定时器——组件卸载或页面跳转时,要调用clearTimeout/clearInterval/cancelAnimationFrame。不然回调还在执行,轻则内存泄漏,重则报错。
4.4 总结
说白了,选择哪种方式取决于你的场景:
- 简单的一次性延迟任务 → setTimeout
- 不要求精确的重复任务 → setInterval
- 需要流畅动画 → requestAnimationFrame
我个人建议,只要是跟视觉相关的动画,一律用rAF。它不仅是性能最优解,而且代码写起来也更优雅。你想想看,一个动画如果跑起来卡卡的,用户第一反应就是「这网站好烂」。所以,别省那几行代码,用rAF就对了。
下一章我们会用rAF做一个更复杂的动画效果——粒子系统。到时候你会发现,rAF的真正威力远不止于数字跳动这么简单。