requestAnimationFrame 执行频率分析
requestAnimationFrame(animateScroll) 的执行频率不固定,而是与显示器刷新率同步。
核心原理
requestAnimationFrame 是浏览器提供的 API,它会:
- 与显示器刷新率同步执行回调函数
- 通常在 60Hz 的显示器上,大约每 16.67ms 执行一次(1000ms ÷ 60 ≈ 16.67ms)
- 在高刷新率显示器上(如 120Hz),执行频率会更高(约 8.33ms)
在 ScrollArea.vue 中,animateScroll 函数通过递归调用 requestAnimationFrame 实现平滑滚动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const animateScroll = () => { if (paused.value || !scrollerRef.value || !shouldScroll.value) { return; }
let currentY = parseFloat(getComputedStyle(scrollerRef.value).transform.split(',')[5] || '0');
let newY = currentY - props.speed;
if (-currentY >= singleListHeight) { newY += singleListHeight; }
scrollerRef.value.style.transform = `translateY(${newY}px)`;
animationFrameId = requestAnimationFrame(animateScroll); };
|
滚动速度控制
虽然 requestAnimationFrame 的执行频率不固定,但组件通过 props.speed 参数控制每次执行时的滚动距离:
- props.speed = 0.5(默认值)表示每次动画帧滚动 0.5px
- 在 60Hz 显示器上,每秒滚动距离为:
0.5px × 60 = 30px/秒
- 可以通过调整
speed 属性来改变滚动速度
性能优势
使用 requestAnimationFrame 的好处:
- 节能:页面不可见时自动暂停
- 流畅:与显示器刷新率同步,避免掉帧
- 优化:浏览器会自动优化动画性能
总结
requestAnimationFrame 的执行频率与显示器刷新率同步
- 通常在 60Hz 显示器上约每 16.67ms 执行一次
- 组件通过
speed 参数控制每次滚动的距离,从而控制整体滚动速度
- 当前默认配置下(speed=0.5),在 60Hz 显示器上的滚动速度约为 30px/秒
和setInterval对比
是否可以固定执行频率
核心结论
不能直接通过 requestAnimationFrame 固定执行频率,因为它的执行频率由浏览器和显示器刷新率决定。但可以通过以下方式实现固定频率:
方案一:使用 setInterval 替代
将 requestAnimationFrame 替换为 setInterval 可以实现固定频率执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| const intervalTime = 50; let intervalId: number | null = null;
const animateScroll = () => { if (paused.value || !scrollerRef.value || !shouldScroll.value) { return; } let currentY = parseFloat(getComputedStyle(scrollerRef.value).transform.split(',')[5] || '0'); let newY = currentY - props.speed; if (-currentY >= singleListHeight) { newY += singleListHeight; } scrollerRef.value.style.transform = `translateY(${newY}px)`; };
const startAnimation = () => { stopAnimation(); if (shouldScroll.value && !paused.value) { intervalId = setInterval(animateScroll, intervalTime); } };
const stopAnimation = () => { if (intervalId !== null) { clearInterval(intervalId); intervalId = null; } };
|
方案二:结合时间戳的混合方案
使用 requestAnimationFrame 但根据时间戳控制执行频率:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| let lastTime = 0; const targetInterval = 50;
const animateScroll = (timestamp: number) => { if (paused.value || !scrollerRef.value || !shouldScroll.value) { animationFrameId = requestAnimationFrame(animateScroll); return; } const elapsed = timestamp - lastTime; if (elapsed >= targetInterval) { let currentY = parseFloat(getComputedStyle(scrollerRef.value).transform.split(',')[5] || '0'); let newY = currentY - props.speed; if (-currentY >= singleListHeight) { newY += singleListHeight; } scrollerRef.value.style.transform = `translateY(${newY}px)`; lastTime = timestamp; } animationFrameId = requestAnimationFrame(animateScroll); };
|
两种方法对比
| 特性 |
requestAnimationFrame |
setInterval |
| 执行频率 |
与显示器刷新率同步(60Hz≈16.67ms) |
精确固定(如50ms) |
| 性能 |
与浏览器渲染同步,更流畅 |
可能与渲染不同步,导致掉帧 |
| 节能 |
页面不可见时自动暂停 |
持续运行,消耗资源 |
| 适用场景 |
视觉动画、UI渲染 |
后台任务、精确计时 |
建议
- 视觉滚动动画:建议继续使用
requestAnimationFrame,保持与显示器同步的流畅性
- 需要精确计时:使用
setInterval,但注意可能的性能问题
- 混合方案:对于需要控制速度但又想保持流畅的场景,使用时间戳方案