javascript如何实现动画效果_requestAnimationFrame为什么比setTimeout更好?

12次阅读

requestAnimationFrame 比 setTimeout 更适合动画,因其自动对齐屏幕刷新率、避免掉帧卡顿、后台暂停省电、支持浏览器统一优化;setTimeout 则易受线程阻塞影响,导致帧率不稳和时间漂移。

javascript如何实现动画效果_requestAnimationFrame为什么比setTimeout更好?

requestAnimationFrame 为什么比 setTimeout 更适合做动画

因为 requestAnimationFrame浏览器专门为动画设计的调度机制,它会自动对齐屏幕刷新率(通常是 60Hz),而 setTimeout 完全依赖开发者手动控制间隔,容易导致掉帧、卡顿或过度绘制。

常见错误现象:用 setTimeout(fn, 16) 模拟 60fps 动画,但实际执行时间受 js 主线程阻塞、任务队列延迟影响,帧率可能骤降到 30fps 甚至更低;同时多个 setTimeout 动画叠加时,还可能出现时间漂移——比如本该在第 5 帧结束的动画,拖到第 7 帧才停。

  • requestAnimationFrame 会在下一次重绘前执行回调,保证每次更新都“赶得上”渲染流水线
  • 页面切换到后台标签页时,requestAnimationFrame 自动暂停,不浪费 CPU;setTimeout 仍会持续触发
  • 浏览器可对 requestAnimationFrame 做统一节流和优化(如降频到 30fps 保续航),setTimeout 无法参与这种协调

如何用 requestAnimationFrame 实现一个基础位移动画

核心是递归调用自身,并在每次回调中计算当前状态、更新样式、决定是否继续。

function animateElement(el, fromX, toX, duration = 300) {   const startTime = performance.now();   const startLeft = fromX;   const distance = toX - fromX; 

function step(timestamp) { const elapsed = timestamp - startTime; const progress = Math.min(elapsed / duration, 1); // 防止超调 const currentX = startLeft + distance * progress;

el.style.transform = `translateX(${currentX}px)`;  if (progress < 1) {   requestAnimationFrame(step); }

}

立即学习Java免费学习笔记(深入)”;

requestAnimationFrame(step); }

// 使用示例 const box = document.getElementById('my-box'); animateElement(box, 0, 200, 400);

注意点:

  • 必须用 performance.now() 计算真实耗时,不能靠帧数 × 16ms 推算(因帧率不恒定)
  • 要显式判断 progress 来终止递归,否则动画停不下来
  • 推荐用 transform 而非 left/top,避免触发重排(layout)

requestAnimationFrame 在循环动画中的常见陷阱

最典型的问题是「重复启动」和「未清理」:用户快速点击多次动画按钮,导致多个 requestAnimationFrame并发运行,互相干扰样式值。

正确做法是保存当前动画帧 ID,并在新动画开始前取消旧的:

let animationId = null; 

function startMoving(el, targetX) { cancelAnimationFrame(animationId); // 关键:先清旧任务

const startX = parseFloat(getComputedStyle(el).transform.split(',')[4]) || 0; const startTime = performance.now();

function step(timestamp) { const elapsed = timestamp - startTime; const progress = Math.min(elapsed / 300, 1); const currentX = startX + (targetX - startX) * progress;

el.style.transform = `translateX(${currentX}px)`;  if (progress < 1) {   animationId = requestAnimationFrame(step); } else {   animationId = null; // 清空 ID,便于下次判断 }

}

立即学习Java免费学习笔记(深入)”;

animationId = requestAnimationFrame(step); }

  • 每个动画实例应独占一个 animationId 变量,不要共用全局 ID
  • 如果动画逻辑复杂(如含 easing 函数、多属性联动),建议封装成类,把 animationId 和状态存在实例属性里
  • 不要在 step 回调里直接修改 dom 属性以外的东西(比如发请求、改全局变量),否则可能破坏帧一致性

什么时候其实不该用 requestAnimationFrame

不是所有“动起来”的需求都适合 requestAnimationFrame。它本质是“每帧都要跑一次”的机制,开销比预期高。

  • 仅需一次视觉反馈(如按钮点击涟漪、toast 弹出)→ 用 css transition@keyframes 更轻量
  • 低频状态变化(如倒计时数字每秒更新一次)→ setIntervalsetTimeout 更合适,强行用 requestAnimationFrame 反而增加主线程负担
  • 需要精确控制毫秒级时序(如音频可视化同步)→ 得结合 AudioContext.currentTimeperformance.now() 做校准,单靠 requestAnimationFrame 不够准

真正需要它的场景很明确:连续、高频、需与屏幕刷新强同步的视觉变化——比如滚动视差、粒子系统、手绘轨迹、canvas 游戏主循环。

text=ZqhQzanResources