全部学科
Python全栈
python
NodeJS全栈
nodejs
小程序首页
📅 2026-05-16 8 分钟 ✍️ juanwangdev

requestAnimationFrame与JS动画

requestAnimationFrame(rAF)是浏览器提供的动画专用API,比setInterval/setTimeout更适合动画场景。

rAF核心原理

什么是rAF

在浏览器下一次重绘之前调用指定回调函数。

JavaScript
function animate() {
  // 更新动画状态
  element.style.transform = `translateX(${x}px)`;

  // 继续下一帧
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

执行时机

JavaScript
浏览器渲染循环:
┌─────────────────────────────────────┐
│  rAF回调执行 ←─────────────────────┐│
│  Style → Layout → Paint → Composite││
└─────────────────────────────────────┘│
          ↓                            │
     显示到屏幕 ────────────────────────┘

与setInterval对比

特性setIntervalrequestAnimationFrame
执行时机固定间隔渲染帧前
帧率同步是(60fps)
后台暂停
性能优化浏览器自动优化

rAF基础用法

基础循环动画

JavaScript
let x = 0;
const element = document.querySelector('.box');

function animate() {
  x += 1;
  element.style.transform = `translateX(${x}px)`;

  if (x < 500) {
    requestAnimationFrame(animate);
  }
}

requestAnimationFrame(animate);

带时间控制的动画

JavaScript
const element = document.querySelector('.box');
const duration = 1000;  // 1秒
const startX = 0;
const endX = 500;

let startTime = null;

function animate(timestamp) {
  if (!startTime) startTime = timestamp;

  const progress = timestamp - startTime;
  const x = startX + (endX - startX) * (progress / duration);

  element.style.transform = `translateX(${Math.min(x, endX)}px)`;

  if (progress < duration) {
    requestAnimationFrame(animate);
  }
}

requestAnimationFrame(animate);

使用参数获取时间戳

JavaScript
// timestamp参数:从页面加载开始的高精度时间
function animate(timestamp) {
  console.log('当前时间:', timestamp);
  console.log('距上次rAF:', timestamp - lastTimestamp);

  requestAnimationFrame(animate);
}

高级技巧

帧率计算

JavaScript
let lastTime = 0;
let frameCount = 0;

function monitorFPS(timestamp) {
  frameCount++;

  if (timestamp - lastTime >= 1000) {
    console.log('FPS:', frameCount);
    frameCount = 0;
    lastTime = timestamp;
  }

  requestAnimationFrame(monitorFPS);
}

requestAnimationFrame(monitorFPS);

动画队列管理

JavaScript
const animations = [];

function runAnimations(timestamp) {
  animations.forEach(anim => {
    anim.update(timestamp);
  });

  // 过滤完成的动画
  animations = animations.filter(anim => !anim.completed);

  if (animations.length > 0) {
    requestAnimationFrame(runAnimations);
  }
}

function addAnimation(updateFn) {
  animations.push({
    update: updateFn,
    completed: false
  });

  if (animations.length === 1) {
    requestAnimationFrame(runAnimations);
  }
}

暂停和恢复

CSS
let animationId = null;
let paused = false;
let pauseTime = 0;
let startTime = null;

function animate(timestamp) {
  if (paused) return;

  if (!startTime) startTime = timestamp;
  const elapsed = timestamp - startTime - pauseTime;

  // 动画逻辑...

  animationId = requestAnimationFrame(animate);
}

function pause() {
  paused = true;
  pauseTime = performance.now();
  cancelAnimationFrame(animationId);
}

function resume() {
  if (paused) {
    paused = false;
    pauseTime += performance.now() - pauseTime;
    requestAnimationFrame(animate);
  }
}

与CSS动画配合

CSS过渡触发JS回调

JavaScript
.box {
  transition: transform 1s;
}

.box.moved {
  transform: translateX(500px);
}
CSS
const box = document.querySelector('.box');

// 触发CSS动画
box.classList.add('moved');

// 监控CSS动画进度
box.addEventListener('transitionstart', () => {
  console.log('动画开始');
});

box.addEventListener('transitionend', () => {
  console.log('动画结束');
});

// 使用rAF同步其他元素
function syncElements(timestamp) {
  // CSS动画进行中时同步其他元素
  const progress = getComputedStyle(box).transform;

  requestAnimationFrame(syncElements);
}

CSS动画+JS微调

JavaScript
.box {
  transition: transform 0.5s;
}
JavaScript
// JS控制动画触发时机
let ready = false;

function triggerAnimation() {
  if (ready) {
    box.style.transform = `translateX(${targetX}px)`;
  }
}

// 确保在渲染帧前设置
requestAnimationFrame(triggerAnimation);

性能优化实践

批量更新

JavaScript
// 错误:每帧多次DOM操作
function animate() {
  element.style.transform = `translateX(${x}px)`;
  element.style.opacity = opacity;
  element.style.width = `${width}px`;

  requestAnimationFrame(animate);
}

// 正确:单次设置
function animate() {
  element.style.cssText = `
    transform: translateX(${x}px);
    opacity: ${opacity};
  `;

  requestAnimationFrame(animate);
}

使用FastDOM模式

JavaScript
const reads = [];
const writes = [];

function measure(fn) {
  reads.push(fn);
}

function mutate(fn) {
  writes.push(fn);
}

function flush() {
  // 先批量读取
  reads.forEach(fn => fn());
  reads.length = 0;

  // 再批量写入
  writes.forEach(fn => fn());
  writes.length = 0;

  requestAnimationFrame(flush);
}

// 使用
measure(() => {
  height = element.offsetHeight;
});

mutate(() => {
  otherElement.style.height = `${height}px`;
});

requestAnimationFrame(flush);

避免强制同步布局

JavaScript
// 错误:读写在同一帧
function animate() {
  const width = element.offsetWidth;  // 强制布局
  element.style.width = `${width + 1}px`;  // 写入
  requestAnimationFrame(animate);
}

// 正确:读写分离
let widthCache = 0;

function measure() {
  widthCache = element.offsetWidth;
}

function animate() {
  element.style.width = `${widthCache + 1}px`;
  measure();  // 为下一帧准备
  requestAnimationFrame(animate);
}

兼容性处理

降级方案

JavaScript
// 兼容性检测
const rAF = window.requestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            function(callback) {
              return setTimeout(callback, 16);
            };

const cAF = window.cancelAnimationFrame ||
            window.webkitCancelAnimationFrame ||
            window.mozCancelAnimationFrame ||
            function(id) {
              clearTimeout(id);
            };

// 使用
const animationId = rAF(animate);
cAF(animationId);

实战示例

拖拽动画

JavaScript
let isDragging = false;
let startX, startY;
let currentX = 0, currentY = 0;

element.addEventListener('mousedown', (e) => {
  isDragging = true;
  startX = e.clientX - currentX;
  startY = e.clientY - currentY;
});

function drag(e) {
  if (!isDragging) return;

  currentX = e.clientX - startX;
  currentY = e.clientY - startY;

  element.style.transform = `translate(${currentX}px, ${currentY}px)`;

  requestAnimationFrame(() => drag(e));
}

document.addEventListener('mousemove', (e) => {
  if (isDragging) {
    requestAnimationFrame(() => drag(e));
  }
});

document.addEventListener('mouseup', () => {
  isDragging = false;
});

滚动动画

text
function smoothScroll(targetY, duration = 500) {
  const startY = window.scrollY;
  const startTime = performance.now();

  function scroll(timestamp) {
    const elapsed = timestamp - startTime;
    const progress = Math.min(elapsed / duration, 1);

    // easeInOut缓动
    const eased = progress < 0.5
      ? 2 * progress * progress
      : 1 - Math.pow(-2 * progress + 2, 2) / 2;

    const currentY = startY + (targetY - startY) * eased;

    window.scrollTo(0, currentY);

    if (progress < 1) {
      requestAnimationFrame(scroll);
    }
  }

  requestAnimationFrame(scroll);
}

smoothScroll(500);

要点总结

  1. rAF在渲染帧前执行,与浏览器帧率同步
  2. 使用timestamp参数获取精确时间
  3. cancelAnimationFrame取消未执行的回调
  4. 后台标签页自动暂停,节省资源
  5. 批量读取、批量写入避免布局抖动
  6. 比setInterval更适合动画场景

📝 发现内容有误?点击此处直接编辑

← 上一篇 CSS动画原理与硬件加速
下一篇 → transform与opacity优化
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库