setInterval 动画

setInterval是最早用于创建Canvas动画的方法之一,虽然现代开发推荐使用requestAnimationFrame,但理解setInterval动画仍有重要意义。

什么是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

动画演示

setInterval 动画

帧率设置

常用帧率

FPS间隔(ms)说明
6016.67流畅动画标准
3033.33普通动画
2441.67电影帧率
1283.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 递归

setTimeout递归调用是setInterval的替代方案,可以更精确地控制帧间隔。

基本结构

function animate() {
  update()
  draw()
  setTimeout(animate, 16)
}

animate()

与setInterval的区别

特性setIntervalsetTimeout递归
执行时机固定间隔上次执行完成后
时间累积会累积不会累积
精确度较低较高
异常处理可能堆积自动跳过

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的问题

1. 时间不精确

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)

2. 帧率不稳定

浏览器可能因为其他任务而延迟执行。

// 模拟帧率不稳定
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

3. 回调堆积

如果回调执行时间超过间隔,会导致回调堆积。

// 危险:回调堆积
setInterval(() => {
  // 模拟耗时操作
  const start = Date.now()
  while (Date.now() - start < 30) {}  // 阻塞30ms
  
  console.log('执行完成')
}, 16)  // 间隔16ms,但执行需要30ms

4. 页面不可见时继续运行

setInterval在页面隐藏时仍会执行,浪费资源。

// 解决方案:监听页面可见性
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    clearInterval(intervalId)
  } else {
    intervalId = setInterval(animate, 16)
  }
})

最佳实践

使用requestAnimationFrame替代

// 不推荐
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
    }
  }
}

使用WeakMap避免内存泄漏

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 Polyfill

// 为旧浏览器提供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)
    }
}