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简洁,对吧?但坑也更多。

注意:setInterval有个致命问题——如果回调执行时间超过了间隔时间,回调会排队等待。这会导致回调连续执行,动画瞬间「跳帧」。我曾经在一个倒计时项目里踩过这个坑,用户反馈倒计时最后几秒突然加速,排查了半天才发现是setInterval的队列问题。

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
  • 更精确的时间戳:回调参数中会传入一个高精度时间戳,方便做时间计算
核心要点:rAF不是「定时器」,而是「渲染前的通知」。它不保证每秒执行60次,而是跟随浏览器的刷新率。如果你的显示器是144Hz,rAF每秒就会执行144次。

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那样一顿一顿的。

小技巧:如果你需要控制动画的帧率(比如只想每秒更新30次),可以在rAF回调里做时间判断:
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的真正威力远不止于数字跳动这么简单。