深入学习requestAnimationFrame的高效使用,实现流畅的Canvas动画循环 requestAnimationFrame是浏览器提供的专门用于动画的API,它能够在最佳时机执行回调,实现流畅的动画效果。
const animationMethodsComparison = {
requestAnimationFrame: {
pros: [
'与浏览器刷新率同步',
'自动节流,页面不可见时暂停',
'更精确的时间控制',
'更流畅的动画效果'
],
cons: [
'需要手动管理取消',
'不支持IE9及以下'
],
useCase: '动画、游戏循环、实时渲染'
},
setTimeout: {
pros: [
'兼容性好',
'可设置任意延迟'
],
cons: [
'不与刷新率同步',
'可能造成帧丢失',
'页面不可见时仍执行'
],
useCase: '延迟执行、定时任务'
},
setInterval: {
pros: [
'自动重复执行'
],
cons: [
'不精确的时间间隔',
'可能造成回调堆积',
'难以动态调整频率'
],
useCase: '轮询、周期性任务'
}
}
class AnimationLoop {
constructor(callback) {
this.callback = callback
this.animationId = null
this.isRunning = false
this.lastTime = 0
this.deltaTime = 0
this.frameCount = 0
this.fps = 0
this.fpsUpdateTime = 0
}
start() {
if (this.isRunning) return
this.isRunning = true
this.lastTime = performance.now()
this.fpsUpdateTime = this.lastTime
this.loop()
}
stop() {
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
this.isRunning = false
}
loop() {
if (!this.isRunning) return
const currentTime = performance.now()
this.deltaTime = (currentTime - this.lastTime) / 1000
this.lastTime = currentTime
this.frameCount++
if (currentTime - this.fpsUpdateTime >= 1000) {
this.fps = this.frameCount
this.frameCount = 0
this.fpsUpdateTime = currentTime
}
this.callback(this.deltaTime, this.fps)
this.animationId = requestAnimationFrame(() => this.loop())
}
getFPS() {
return this.fps
}
getDeltaTime() {
return this.deltaTime
}
}
class FixedFrameRateLoop {
constructor(callback, targetFPS = 60) {
this.callback = callback
this.targetFPS = targetFPS
this.frameInterval = 1000 / targetFPS
this.animationId = null
this.isRunning = false
this.lastTime = 0
this.accumulator = 0
}
start() {
if (this.isRunning) return
this.isRunning = true
this.lastTime = performance.now()
this.accumulator = 0
this.loop()
}
stop() {
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
this.isRunning = false
}
loop() {
if (!this.isRunning) return
const currentTime = performance.now()
const elapsed = currentTime - this.lastTime
this.lastTime = currentTime
this.accumulator += elapsed
while (this.accumulator >= this.frameInterval) {
this.callback(this.frameInterval / 1000)
this.accumulator -= this.frameInterval
}
this.animationId = requestAnimationFrame(() => this.loop())
}
setTargetFPS(fps) {
this.targetFPS = fps
this.frameInterval = 1000 / fps
}
}
class AdaptiveFrameRateLoop {
constructor(callback, options = {}) {
this.callback = callback
this.options = {
minFPS: 15,
maxFPS: 60,
targetFrameTime: 16.67,
...options
}
this.animationId = null
this.isRunning = false
this.lastTime = 0
this.frameTimeHistory = []
this.historySize = 10
this.currentQuality = 1
}
start() {
if (this.isRunning) return
this.isRunning = true
this.lastTime = performance.now()
this.frameTimeHistory = []
this.loop()
}
stop() {
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
this.isRunning = false
}
loop() {
if (!this.isRunning) return
const currentTime = performance.now()
const deltaTime = currentTime - this.lastTime
this.lastTime = currentTime
this.frameTimeHistory.push(deltaTime)
if (this.frameTimeHistory.length > this.historySize) {
this.frameTimeHistory.shift()
}
this.adjustQuality()
this.callback(deltaTime / 1000, this.currentQuality)
this.animationId = requestAnimationFrame(() => this.loop())
}
adjustQuality() {
if (this.frameTimeHistory.length < this.historySize) return
const avgFrameTime = this.frameTimeHistory.reduce((a, b) => a + b, 0) / this.frameTimeHistory.length
const targetFrameTime = 1000 / this.options.maxFPS
const maxFrameTime = 1000 / this.options.minFPS
if (avgFrameTime > maxFrameTime && this.currentQuality > 0.25) {
this.currentQuality -= 0.1
} else if (avgFrameTime < targetFrameTime * 0.8 && this.currentQuality < 1) {
this.currentQuality += 0.1
}
this.currentQuality = Math.max(0.25, Math.min(1, this.currentQuality))
}
getQuality() {
return this.currentQuality
}
}
class FixedTimeStepLoop {
constructor(updateCallback, renderCallback, fixedDeltaTime = 1/60) {
this.updateCallback = updateCallback
this.renderCallback = renderCallback
this.fixedDeltaTime = fixedDeltaTime
this.animationId = null
this.isRunning = false
this.lastTime = 0
this.accumulator = 0
this.maxAccumulator = 0.25
this.totalTime = 0
this.frameCount = 0
}
start() {
if (this.isRunning) return
this.isRunning = true
this.lastTime = performance.now()
this.accumulator = 0
this.loop()
}
stop() {
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
this.isRunning = false
}
loop() {
if (!this.isRunning) return
const currentTime = performance.now()
let deltaTime = (currentTime - this.lastTime) / 1000
this.lastTime = currentTime
if (deltaTime > this.maxAccumulator) {
deltaTime = this.maxAccumulator
}
this.accumulator += deltaTime
while (this.accumulator >= this.fixedDeltaTime) {
this.updateCallback(this.fixedDeltaTime)
this.totalTime += this.fixedDeltaTime
this.frameCount++
this.accumulator -= this.fixedDeltaTime
}
const alpha = this.accumulator / this.fixedDeltaTime
this.renderCallback(alpha)
this.animationId = requestAnimationFrame(() => this.loop())
}
getStats() {
return {
totalTime: this.totalTime,
frameCount: this.frameCount,
avgFPS: this.frameCount / this.totalTime
}
}
}
class InterpolatedRenderLoop {
constructor(canvas) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')
this.entities = []
this.animationId = null
this.isRunning = false
this.lastTime = 0
this.accumulator = 0
this.fixedDeltaTime = 1/60
}
addEntity(entity) {
this.entities.push(entity)
}
start() {
if (this.isRunning) return
this.isRunning = true
this.lastTime = performance.now()
this.loop()
}
stop() {
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
this.isRunning = false
}
loop() {
if (!this.isRunning) return
const currentTime = performance.now()
const deltaTime = (currentTime - this.lastTime) / 1000
this.lastTime = currentTime
this.accumulator += deltaTime
while (this.accumulator >= this.fixedDeltaTime) {
this.update(this.fixedDeltaTime)
this.accumulator -= this.fixedDeltaTime
}
const alpha = this.accumulator / this.fixedDeltaTime
this.render(alpha)
this.animationId = requestAnimationFrame(() => this.loop())
}
update(dt) {
this.entities.forEach(entity => {
entity.prevX = entity.x
entity.prevY = entity.y
entity.update(dt)
})
}
render(alpha) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.entities.forEach(entity => {
const renderX = entity.prevX + (entity.x - entity.prevX) * alpha
const renderY = entity.prevY + (entity.y - entity.prevY) * alpha
entity.render(this.ctx, renderX, renderY)
})
}
}
class PausableAnimationLoop {
constructor(callback) {
this.callback = callback
this.animationId = null
this.isRunning = false
this.isPaused = false
this.lastTime = 0
this.pauseTime = 0
this.totalPausedTime = 0
}
start() {
if (this.isRunning) return
this.isRunning = true
this.isPaused = false
this.lastTime = performance.now()
this.totalPausedTime = 0
this.loop()
}
stop() {
this.pause()
this.isRunning = false
}
pause() {
if (!this.isRunning || this.isPaused) return
this.isPaused = true
this.pauseTime = performance.now()
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
}
resume() {
if (!this.isRunning || !this.isPaused) return
this.isPaused = false
this.totalPausedTime += performance.now() - this.pauseTime
this.lastTime = performance.now()
this.loop()
}
toggle() {
if (this.isPaused) {
this.resume()
} else {
this.pause()
}
}
loop() {
if (!this.isRunning || this.isPaused) return
const currentTime = performance.now()
const deltaTime = (currentTime - this.lastTime) / 1000
this.lastTime = currentTime
this.callback(deltaTime)
this.animationId = requestAnimationFrame(() => this.loop())
}
getElapsed() {
if (!this.isRunning) return 0
if (this.isPaused) {
return (this.pauseTime - this.lastTime - this.totalPausedTime) / 1000
}
return (performance.now() - this.lastTime - this.totalPausedTime) / 1000
}
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas requestAnimationFrame - 动画框架</title>
<style>
body {
margin: 0;
padding: 20px;
background: #1a1a2e;
font-family: system-ui, sans-serif;
}
.container {
max-width: 900px;
margin: 0 auto;
}
h1 {
color: #eee;
text-align: center;
}
.canvas-wrapper {
background: #16213e;
border-radius: 8px;
padding: 10px;
margin-bottom: 20px;
}
canvas {
display: block;
background: #0f0f23;
border-radius: 4px;
margin: 0 auto;
}
.stats {
color: #00ff00;
font-family: monospace;
font-size: 12px;
text-align: center;
margin-top: 10px;
}
.controls {
display: flex;
gap: 10px;
justify-content: center;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
background: #e94560;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #ff6b6b;
}
.slider-group {
display: flex;
align-items: center;
gap: 10px;
color: #eee;
}
input[type="range"] {
width: 100px;
}
</style>
</head>
<body>
<div class="container">
<h1>requestAnimationFrame 动画框架</h1>
<div class="canvas-wrapper">
<canvas id="mainCanvas" width="800" height="400"></canvas>
<div class="stats" id="stats">FPS: 0 | 帧时间: 0ms | 质量: 100%</div>
</div>
<div class="controls">
<button onclick="toggleAnimation()">开始/暂停</button>
<button onclick="resetAnimation()">重置</button>
<div class="slider-group">
<label>目标FPS:</label>
<input type="range" id="fpsSlider" min="15" max="60" value="60" onchange="setTargetFPS(this.value)">
<span id="fpsValue">60</span>
</div>
<div class="slider-group">
<label>粒子数:</label>
<input type="range" id="particleSlider" min="50" max="500" value="200" onchange="setParticleCount(this.value)">
<span id="particleValue">200</span>
</div>
</div>
</div>
<script>
class Particle {
constructor(canvasWidth, canvasHeight) {
this.reset(canvasWidth, canvasHeight)
}
reset(canvasWidth, canvasHeight) {
this.x = Math.random() * canvasWidth
this.y = Math.random() * canvasHeight
this.prevX = this.x
this.prevY = this.y
this.vx = (Math.random() - 0.5) * 100
this.vy = (Math.random() - 0.5) * 100
this.radius = Math.random() * 4 + 2
this.hue = Math.random() * 360
}
update(dt, canvasWidth, canvasHeight) {
this.prevX = this.x
this.prevY = this.y
this.x += this.vx * dt
this.y += this.vy * dt
if (this.x < this.radius || this.x > canvasWidth - this.radius) {
this.vx *= -1
this.x = Math.max(this.radius, Math.min(canvasWidth - this.radius, this.x))
}
if (this.y < this.radius || this.y > canvasHeight - this.radius) {
this.vy *= -1
this.y = Math.max(this.radius, Math.min(canvasHeight - this.radius, this.y))
}
}
render(ctx, alpha = 1) {
const renderX = this.prevX + (this.x - this.prevX) * alpha
const renderY = this.prevY + (this.y - this.prevY) * alpha
ctx.beginPath()
ctx.arc(renderX, renderY, this.radius, 0, Math.PI * 2)
ctx.fillStyle = `hsla(${this.hue}, 70%, 60%, 0.8)`
ctx.fill()
}
}
class AnimationFramework {
constructor(canvas) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')
this.particles = []
this.targetFPS = 60
this.fixedDeltaTime = 1/60
this.animationId = null
this.isRunning = false
this.lastTime = 0
this.accumulator = 0
this.frameCount = 0
this.fps = 0
this.fpsUpdateTime = 0
this.frameTime = 0
this.quality = 1
this.init()
}
init() {
this.createParticles(200)
}
createParticles(count) {
this.particles = []
for (let i = 0; i < count; i++) {
this.particles.push(new Particle(this.canvas.width, this.canvas.height))
}
}
start() {
if (this.isRunning) return
this.isRunning = true
this.lastTime = performance.now()
this.accumulator = 0
this.fpsUpdateTime = this.lastTime
this.loop()
}
stop() {
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
this.isRunning = false
}
toggle() {
if (this.isRunning) {
this.stop()
} else {
this.start()
}
}
loop() {
if (!this.isRunning) return
const currentTime = performance.now()
const deltaTime = (currentTime - this.lastTime) / 1000
this.lastTime = currentTime
this.frameTime = deltaTime * 1000
this.frameCount++
if (currentTime - this.fpsUpdateTime >= 1000) {
this.fps = this.frameCount
this.frameCount = 0
this.fpsUpdateTime = currentTime
}
this.accumulator += deltaTime
const maxIterations = 5
let iterations = 0
while (this.accumulator >= this.fixedDeltaTime && iterations < maxIterations) {
this.update(this.fixedDeltaTime)
this.accumulator -= this.fixedDeltaTime
iterations++
}
const alpha = this.accumulator / this.fixedDeltaTime
this.render(alpha)
this.adjustQuality(deltaTime)
this.animationId = requestAnimationFrame(() => this.loop())
}
update(dt) {
this.particles.forEach(p => {
p.update(dt, this.canvas.width, this.canvas.height)
})
}
render(alpha) {
this.ctx.fillStyle = 'rgba(15, 15, 35, 0.3)'
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
const renderCount = Math.ceil(this.particles.length * this.quality)
for (let i = 0; i < renderCount; i++) {
this.particles[i].render(this.ctx, alpha)
}
}
adjustQuality(deltaTime) {
const targetFrameTime = 1 / this.targetFPS
if (deltaTime > targetFrameTime * 1.5 && this.quality > 0.3) {
this.quality -= 0.05
} else if (deltaTime < targetFrameTime * 0.8 && this.quality < 1) {
this.quality += 0.02
}
this.quality = Math.max(0.3, Math.min(1, this.quality))
}
setTargetFPS(fps) {
this.targetFPS = fps
this.fixedDeltaTime = 1 / fps
}
setParticleCount(count) {
this.createParticles(count)
}
reset() {
this.createParticles(this.particles.length)
this.quality = 1
}
getStats() {
return {
fps: this.fps,
frameTime: this.frameTime.toFixed(2),
quality: (this.quality * 100).toFixed(0)
}
}
}
const canvas = document.getElementById('mainCanvas')
const stats = document.getElementById('stats')
const fpsSlider = document.getElementById('fpsSlider')
const fpsValue = document.getElementById('fpsValue')
const particleSlider = document.getElementById('particleSlider')
const particleValue = document.getElementById('particleValue')
const framework = new AnimationFramework(canvas)
function updateStats() {
const s = framework.getStats()
stats.textContent = `FPS: ${s.fps} | 帧时间: ${s.frameTime}ms | 质量: ${s.quality}%`
requestAnimationFrame(updateStats)
}
function toggleAnimation() {
framework.toggle()
}
function resetAnimation() {
framework.reset()
}
function setTargetFPS(value) {
framework.setTargetFPS(parseInt(value))
fpsValue.textContent = value
}
function setParticleCount(value) {
framework.setParticleCount(parseInt(value))
particleValue.textContent = value
}
fpsSlider.addEventListener('input', (e) => {
fpsValue.textContent = e.target.value
})
particleSlider.addEventListener('input', (e) => {
particleValue.textContent = e.target.value
})
framework.start()
updateStats()
</script>
</body>
</html>
const rafBestPractices = {
timing: {
title: '时间管理',
tips: [
'使用performance.now()获取精确时间',
'计算deltaTime而非假设固定值',
'限制最大deltaTime防止跳帧',
'使用固定时间步进保证物理稳定'
]
},
performance: {
title: '性能优化',
tips: [
'避免在回调中创建对象',
'使用对象池复用对象',
'根据性能动态调整质量',
'跳过不必要的渲染帧'
]
},
lifecycle: {
title: '生命周期管理',
tips: [
'正确取消动画帧',
'处理页面可见性变化',
'实现暂停/恢复功能',
'清理不再使用的资源'
]
},
compatibility: {
title: '兼容性处理',
tips: [
'提供降级方案(setTimeout)',
'检测浏览器前缀',
'处理不支持的情况'
]
}
}
function getRequestAnimationFrame() {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
return setTimeout(callback, 16.67)
}
}
function getCancelAnimationFrame() {
return window.cancelAnimationFrame ||
window.webkitCancelAnimationFrame ||
window.mozCancelAnimationFrame ||
window.oCancelAnimationFrame ||
window.msCancelAnimationFrame ||
function(id) {
clearTimeout(id)
}
}
class VisibilityAwareLoop {
constructor(callback) {
this.callback = callback
this.animationId = null
this.isRunning = false
this.isPaused = false
this.lastTime = 0
this.setupVisibilityHandler()
}
setupVisibilityHandler() {
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.pause()
} else if (this.isRunning) {
this.resume()
}
})
}
start() {
if (this.isRunning) return
this.isRunning = true
this.isPaused = false
this.lastTime = performance.now()
this.loop()
}
stop() {
this.pause()
this.isRunning = false
}
pause() {
if (!this.isRunning || this.isPaused) return
this.isPaused = true
if (this.animationId) {
getCancelAnimationFrame()(this.animationId)
this.animationId = null
}
}
resume() {
if (!this.isRunning || !this.isPaused) return
this.isPaused = false
this.lastTime = performance.now()
this.loop()
}
loop() {
if (!this.isRunning || this.isPaused) return
const currentTime = performance.now()
const deltaTime = (currentTime - this.lastTime) / 1000
this.lastTime = currentTime
this.callback(deltaTime)
this.animationId = getRequestAnimationFrame()(() => this.loop())
}
}
requestAnimationFrame是Canvas动画的核心API:
合理使用requestAnimationFrame,可以实现流畅、高效的Canvas动画效果。