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对比
| 特性 | setInterval | requestAnimationFrame |
|---|---|---|
| 执行时机 | 固定间隔 | 渲染帧前 |
| 帧率同步 | 否 | 是(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);
要点总结
- rAF在渲染帧前执行,与浏览器帧率同步
- 使用timestamp参数获取精确时间
- cancelAnimationFrame取消未执行的回调
- 后台标签页自动暂停,节省资源
- 批量读取、批量写入避免布局抖动
- 比setInterval更适合动画场景
📝 发现内容有误?点击此处直接编辑