4、JavaScript动画基础:requestAnimationFrame、setTimeout/setInterval对比、性能监控、简单缓动函数实现
聊到跨平台动画,JavaScript 是绕不开的基础。不管你是用 React Native、Flutter 还是 Web 开发,底层动画调度逻辑其实都差不多。今天我就带你把这几个核心概念彻底搞明白。
4.1 为什么不用 setTimeout 做动画?
很多新手喜欢用 setTimeout 或 setInterval 做动画。代码写起来简单,对吧?
let left = 0;
const interval = setInterval(() => {
left += 5;
element.style.transform = `translateX(${left}px)`;
if (left >= 300) clearInterval(interval);
}, 16);
嗯,这里要注意。这段代码看起来每 16ms 执行一次,差不多 60fps。但实际运行起来,你会发现动画一卡一卡的。
为什么会这样?
因为 setTimeout 和 setInterval 根本不关心浏览器渲染周期。它们只保证「至少延迟这么多毫秒后执行」,但不保证「在帧渲染前执行」。我在项目中遇到过这种情况:一个简单的滚动动画,用 setInterval 写完后在低端手机上像幻灯片一样。
说白了,这两个 API 的精度不够。JavaScript 是单线程的,如果主线程有其他任务在跑,你的回调就会被推迟。更糟糕的是,即使回调准时执行了,浏览器也可能因为布局计算、样式重绘等原因错过这一帧。
4.2 requestAnimationFrame:浏览器亲儿子
requestAnimationFrame(简称 rAF)才是做动画的正确姿势。它告诉浏览器:「嘿,下次你要刷新屏幕的时候,记得叫我一声。」
let left = 0;
function animate() {
left += 5;
element.style.transform = `translateX(${left}px)`;
if (left < 300) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
你想想看,rAF 的回调是在浏览器准备渲染下一帧之前执行的。这意味着你的动画更新和浏览器的绘制周期是同步的。不会出现「更新了但没渲染」或者「渲染了但没更新」的尴尬情况。
我个人习惯用 rAF 做所有需要连续更新的场景——动画、游戏循环、甚至一些高频的 UI 状态同步。
- setTimeout/setInterval:不管浏览器死活,到点就执行
- requestAnimationFrame:跟着浏览器节奏走,页面不可见时自动暂停
4.3 性能对比:一张表说清楚
| 特性 | requestAnimationFrame | setTimeout/setInterval |
|---|---|---|
| 执行时机 | 浏览器渲染前一帧 | 延迟到期后尽快执行 |
| 帧率控制 | 自动匹配屏幕刷新率 | 手动设置间隔,容易错帧 |
| 后台行为 | 自动暂停(省电) | 继续执行(浪费性能) |
| 精度 | 高,与 VSync 同步 | 低,受事件队列影响 |
| CPU/GPU 占用 | 优化,页面不可见时停止 | 持续占用,即使页面不可见 |
我建议你记住一句话:能用 rAF 就别用定时器。除非你只是想做「每隔几秒执行一次」的轮询,否则动画场景一律用 rAF。
4.4 性能监控:别靠感觉,靠数据
做动画最怕什么?怕你觉得流畅,用户觉得卡。所以性能监控是必修课。
最简单的办法是计算帧率(FPS)。我们可以用 rAF 自己实现一个 FPS 计数器:
let frameCount = 0;
let lastTime = performance.now();
function monitorFPS() {
frameCount++;
const now = performance.now();
const delta = now - lastTime;
if (delta >= 1000) {
const fps = Math.round((frameCount * 1000) / delta);
console.log(`当前帧率:${fps} FPS`);
frameCount = 0;
lastTime = now;
}
requestAnimationFrame(monitorFPS);
}
requestAnimationFrame(monitorFPS);
我在项目中遇到过一个问题:动画在开发工具里跑得很流畅,但真机上一卡一卡的。后来加上 FPS 监控才发现,原来是某个第三方库在每帧都触发了强制回流。
4.5 简单缓动函数实现
动画光有线性移动是不够的。你想想看,现实世界哪有东西是匀速运动的?
缓动函数(easing function)就是用来模拟这种「加速-减速」效果的。本质上就是一个数学公式,把时间进度映射成位置进度。
先看一个最简单的缓动函数——ease-out(减速退出):
function easeOut(t) {
return 1 - Math.pow(1 - t, 3);
}
// 使用示例
const duration = 1000; // 动画时长 1 秒
const startTime = performance.now();
const startPos = 0;
const endPos = 300;
function animateEaseOut() {
const elapsed = performance.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const easedProgress = easeOut(progress);
const currentPos = startPos + (endPos - startPos) * easedProgress;
element.style.transform = `translateX(${currentPos}px)`;
if (progress < 1) {
requestAnimationFrame(animateEaseOut);
}
}
requestAnimationFrame(animateEaseOut);
这里 easeOut(t) 接收一个 0 到 1 的进度值,返回一个也是 0 到 1 的缓动值。区别在于:t 是线性增长的,但 easeOut(t) 的增长速度会越来越慢。
常用的缓动函数就那么几个,我整理一下:
| 名称 | 公式 | 效果 |
|---|---|---|
| linear | t | 匀速 |
| easeIn | t³ | 慢-快 |
| easeOut | 1 - (1-t)³ | 快-慢 |
| easeInOut | t<0.5 ? 4t³ : 1-(1-t)³ | 慢-快-慢 |
我个人习惯把缓动函数封装成一个工具库,这样在多个项目里复用。比如在 React Native 里,我会用 Animated.timing 配合 easing 模块;在 Web 里,我会自己写一套类似的接口。
嗯,到这里 JavaScript 动画基础就讲完了。下一章我们会把这些知识应用到实际项目中,看看怎么在 Web、React Native 和 Flutter 里写出高性能的动画。