setInterval是最早用于创建Canvas动画的方法之一,虽然现代开发推荐使用requestAnimationFrame,但理解setInterval动画仍有重要意义。
setInterval是JavaScript提供的定时器函数,可以按照指定的时间间隔重复执行代码。
const intervalId = setInterval(callback, delay, ...args)
// callback: 要执行的函数
// delay: 间隔时间(毫秒)
// args: 传递给回调函数的参数
let x = 0
const intervalId = setInterval(() => {
console.log(`x = ${x}`)
x++
}, 1000)
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
let x = 0
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = '#3498db'
ctx.fillRect(x, 100, 50, 50)
x += 2
if (x > canvas.width) x = -50
}
const intervalId = setInterval(animate, 16) // 约60FPS
| FPS | 间隔(ms) | 说明 |
|---|---|---|
| 60 | 16.67 | 流畅动画标准 |
| 30 | 33.33 | 普通动画 |
| 24 | 41.67 | 电影帧率 |
| 12 | 83.33 | 最低流畅度 |
function getFrameInterval(fps) {
return 1000 / fps
}
const fps60 = getFrameInterval(60) // 16.67ms
const fps30 = getFrameInterval(30) // 33.33ms
const animation = {
intervalId: null,
isRunning: false,
start() {
if (!this.isRunning) {
this.isRunning = true
this.intervalId = setInterval(this.animate, 16)
}
},
stop() {
if (this.isRunning) {
this.isRunning = false
clearInterval(this.intervalId)
this.intervalId = null
}
},
animate() {
// 动画逻辑
}
}
const animation = {
intervalId: null,
isPaused: false,
x: 0,
start() {
this.intervalId = setInterval(() => {
if (!this.isPaused) {
this.update()
this.draw()
}
}, 16)
},
pause() {
this.isPaused = true
},
resume() {
this.isPaused = false
},
stop() {
clearInterval(this.intervalId)
this.intervalId = null
this.isPaused = false
}
}
setTimeout递归调用是setInterval的替代方案,可以更精确地控制帧间隔。
function animate() {
update()
draw()
setTimeout(animate, 16)
}
animate()
| 特性 | setInterval | setTimeout递归 |
|---|---|---|
| 执行时机 | 固定间隔 | 上次执行完成后 |
| 时间累积 | 会累积 | 不会累积 |
| 精确度 | 较低 | 较高 |
| 异常处理 | 可能堆积 | 自动跳过 |
let lastTime = 0
const targetFPS = 60
const frameInterval = 1000 / targetFPS
function animate(currentTime) {
const deltaTime = currentTime - lastTime
if (deltaTime >= frameInterval) {
lastTime = currentTime - (deltaTime % frameInterval)
update(deltaTime)
draw()
}
setTimeout(() => requestAnimationFrame(animate), 0)
}
requestAnimationFrame(animate)
setInterval的实际执行间隔可能比设定值长。
const start = Date.now()
let count = 0
setInterval(() => {
count++
const elapsed = Date.now() - start
const expected = count * 100
console.log(`实际: ${elapsed}ms, 预期: ${expected}ms, 差值: ${elapsed - expected}ms`)
}, 100)
浏览器可能因为其他任务而延迟执行。
// 模拟帧率不稳定
let frameCount = 0
let lastSecond = Date.now()
setInterval(() => {
frameCount++
const now = Date.now()
if (now - lastSecond >= 1000) {
console.log(`实际FPS: ${frameCount}`)
frameCount = 0
lastSecond = now
}
}, 16) // 理论60FPS
如果回调执行时间超过间隔,会导致回调堆积。
// 危险:回调堆积
setInterval(() => {
// 模拟耗时操作
const start = Date.now()
while (Date.now() - start < 30) {} // 阻塞30ms
console.log('执行完成')
}, 16) // 间隔16ms,但执行需要30ms
setInterval在页面隐藏时仍会执行,浪费资源。
// 解决方案:监听页面可见性
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
clearInterval(intervalId)
} else {
intervalId = setInterval(animate, 16)
}
})
// 不推荐
setInterval(animate, 16)
// 推荐
requestAnimationFrame(function loop() {
animate()
requestAnimationFrame(loop)
})
如果必须使用setInterval,可以结合帧率限制:
let lastTime = 0
const targetFPS = 30
const frameInterval = 1000 / targetFPS
function animate(currentTime) {
if (currentTime - lastTime >= frameInterval) {
lastTime = currentTime
update()
draw()
}
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
// 组件卸载时清理
class AnimationComponent {
constructor() {
this.intervalId = null
}
start() {
this.intervalId = setInterval(this.animate, 16)
}
destroy() {
if (this.intervalId) {
clearInterval(this.intervalId)
this.intervalId = null
}
}
}
const animationStates = new WeakMap()
function startAnimation(element) {
const state = {
intervalId: setInterval(() => animate(element), 16),
x: 0
}
animationStates.set(element, state)
}
function stopAnimation(element) {
const state = animationStates.get(element)
if (state) {
clearInterval(state.intervalId)
animationStates.delete(element)
}
}
// 为旧浏览器提供requestAnimationFrame支持
if (!window.requestAnimationFrame) {
window.requestAnimationFrame =
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
return setTimeout(callback, 16)
}
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame =
window.webkitCancelAnimationFrame ||
window.mozCancelAnimationFrame ||
window.msCancelAnimationFrame ||
function(id) {
clearTimeout(id)
}
}