
本文详解 requestanimationframe 在 canvas 动画中的正确调用方式,指出常见误区(如在 update 内部重复调用 requestanimationframe(update) 导致帧率失控),并提供结构清晰、可复用的动画循环实现方案。
本文详解 requestanimationframe 在 canvas 动画中的正确调用方式,指出常见误区(如在 update 内部重复调用 requestanimationframe(update) 导致帧率失控),并提供结构清晰、可复用的动画循环实现方案。
在基于 Canvas 的逐帧动画开发中,requestAnimationFrame(简称 rAF)是实现高性能、与屏幕刷新率同步动画的核心 API。但许多开发者会陷入一个典型陷阱:在动画主函数内部递归调用 requestAnimationFrame(update),这看似“自动循环”,实则破坏了 rAF 的设计契约——它本应由浏览器统一调度,而非由 JavaScript 主动“抢占式”触发。
你提供的代码中,update() 函数开头即执行:
function update() {
requestAnimationFrame(update); // ❌ 错误:此处调用导致不可控的嵌套调用链
frames++;
// ... 其余逻辑
}该写法的问题在于:
- 每次 update 执行都会立即请求下一帧,不等待浏览器实际绘制完成;
- 若 update 执行耗时较长(如图像加载未就绪、计算复杂),可能引发帧堆积或跳帧;
- 更严重的是,requestAnimationFrame 的回调参数应为 单次执行的函数引用,而你在 window.onload 中已调用 requestAnimationFrame(update),此时再于 update 内重复调用,等于“一帧触发多帧”,极易造成动画卡顿、帧序错乱甚至内存泄漏。
✅ 正确做法是:将 rAF 调度逻辑与动画逻辑解耦,确保每帧仅被调度一次,且由浏览器主导节奏。推荐采用以下结构:
// ✅ 推荐:干净、可控的动画循环
let frames = 0;
let lastTime = 0;
function gameLoop(timestamp) {
// 计算自上一帧以来经过的时间(毫秒),可用于时间敏感逻辑
const deltaTime = timestamp - lastTime;
lastTime = timestamp;
// 清屏
context.clearRect(0, 0, board.width, board.height);
// 更新状态(物理、碰撞、计分等)
if (state.current === state.game) {
velocityY += gravity;
dino.y = Math.max(dino.y + velocityY, 0);
collisionDetection();
updateScore();
}
// 绘制所有元素
drawBase();
drawDino();
drawPipes();
// 逐帧动画控制:每 N 帧切换一帧
frames++;
if (state.current === state.game && frames % dino.period === 0) {
dino.frame = (dino.frame + 1) % dino.animation.length;
}
// 关键:在此处请求下一帧 —— 且仅一次
requestAnimationFrame(gameLoop);
}
// 启动动画循环(替代原 setInterval + requestAnimationFrame(update))
window.onload = function() {
board = document.querySelector('#canvas');
scoreElemnt = document.querySelector('#score');
bestScoreElement = document.querySelector('#bestScore');
boardWidth = board.width;
boardHeight = board.height;
context = board.getContext('2d');
setInterval(placePipes, 1500);
// ✅ 正确启动:只调用一次 requestAnimationFrame
requestAnimationFrame(gameLoop);
};? 关键改进点说明:
- gameLoop 是单一入口动画函数,接收 timestamp 参数,便于实现基于时间的平滑动画(如匀速移动);
- frames 计数器与 dino.period 配合,实现“每 5 帧切换一次图片”的节拍控制,避免过快闪烁;
- requestAnimationFrame(gameLoop) 严格置于函数末尾,保证浏览器在完成当前帧渲染后,再调度下一帧,符合事件循环规范;
- 移除了 update 函数内冗余的 requestAnimationFrame(update),杜绝双重调度风险。
⚠️ 额外注意事项:
- 确保所有 Image 对象(如 startRunImg)已在 onload 前完成加载,否则 drawImage 可能静默失败。建议添加 img.onload 回调或使用 Promise.all([...images].map(img => new Promise(r => img.onload = r))) 预加载;
- dino.period = 5 应定义在初始化阶段(如 dino 对象创建时),而非每次 drawDino() 中重复赋值;
- 若需暂停/恢复动画,可通过布尔标志位控制 gameLoop 内部逻辑,而非停止 requestAnimationFrame(避免重启动开销)。
遵循以上模式,你的恐龙逐帧奔跑动画将稳定运行于 60 FPS(或设备最高支持帧率),真正发挥 requestAnimationFrame 的性能优势。










