requestAnimationFrame

逸男2022年8月6日
  • JavaScript
  • js工具箱
  • 性能优化
大约 3 分钟

requestAnimationFrame

它是什么?

window.requestAnimationFrame()告诉浏览器————你希望执行一个动画,并且要求浏览器在下次重绘或者回流前调用指定的回调函数更新动画。 设置这个 API 的目的是为了让各种网页动画效果(DOM 动画、Canvas 动画、SVG 动画、WebGl 动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。

它的优点

requestAnimationFrame对比setTimeoutsetInterval的优势主要有两点:

  1. requestAnimationFrame会把每一帧所有 dom 操作集中起来,在一次重绘或回流中完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒 60 帧。
  2. 在隐藏或者不可见的元素中,requestAnimationFrame将不会进行重绘或者回流,这就意味着更少的 cpu,gpu 和内存的使用量。比如页面隐藏就不会再触发。

使用和取消

requestAnimationFrame要想达到类似setInterval的效果 需要使用类似递归的方式

为什么说是类似递归呢?递归的含义为自己调用自己,而该方法中间有一层requestAnimationFrame调用,不是严格意义上的递归

下面是一个示例,包含如果取消requestAnimationFrame

let i = 0
let frameId = null // requestAnimationFrame返回的id
function addition(time) {
  console.log(++i)
  frameId = requestAnimationFrame(addition)
  // 1自加到达200 取消requestAnimationFrame
  if (i === 200) cancelAnimationFrame(frameId)
}
frameId = requestAnimationFrame(addition)

上面的代码中 requestAnimationFrame 会返回一个整数 id,传入requestAnimationFrame()即可取消 requestAnimationFrame 的 callback 执行时,它的参数是系统传入的一个高精度时间戳(performance.now()的返回值),单位是毫秒,表示距离网页加载的时间。

注意点

  1. 使用类似递归的方式,requestAnimationFrame的运行频率和屏幕的帧率有关,无法像setInterval一样指定触发间隔。
  2. 屏幕只决定了每秒的最大运行频率,当计算量过大时,会出现掉帧的情况,无法达到屏幕帧率的运行频率。

常见的屏幕帧率和运行频率

屏幕帧率运行频率
60hz16.66ms
90hz11.11ms
120hz8.33ms

运行频率(ms) = 1000 / 屏幕帧率

自定义工具类

特点

  1. 支持自定义触发间隔
  2. 支持暂停播放,也可以设置是否自动播放
  3. 支持动态更改 触发间隔 触发函数

工具代码

class MyRequestAnimationFrame {
  _frameId // 帧id
  _prevFrameTime = 0 // 上一次执行时间 ms
  _recordTime = 0 // 时间记录
  _frameSpeed // 触发时间间隔 ms
  _callback
  constructor(callback, frameSpeed, { autoPlay = true, now = false } = {}) {
    this._callback = callback
    this._frameSpeed = frameSpeed
    if (autoPlay) this.play()
    if (now) this._callback(performance.now())
  }
  steCallback(callback) {
    this._callback = callback
  }
  steFrameSpeed(frameSpeed) {
    this._frameSpeed = frameSpeed
  }
  _step(pageTime) {
    if (this._frameSpeed) {
      let frameTime = pageTime - this._prevFrameTime // 每次触发的间隔
      this._prevFrameTime = pageTime
      this._recordTime += frameTime
      if (this._recordTime >= this._frameSpeed) {
        this._callback(pageTime)
        this._recordTime = this._recordTime % this._frameSpeed // 修正时间差
      }
    } else {
      this._callback(pageTime)
    }
    this._frameId = window.requestAnimationFrame(this._step.bind(this))
  }
  play() {
    this._prevFrameTime = performance.now()
    if (!this._frameId) this._frameId = window.requestAnimationFrame(this._step.bind(this))
  }
  stop() {
    if (this._frameId) {
      cancelAnimationFrame(this._frameId)
      this._frameId = null
    }
  }
}

使用示例

const myAnimation = new MyRequestAnimationFrame(
  pageTime => {
    console.log(pageTime)
  },
  200,
  {
    autoPlay: false,
    now: true,
  },
)
// 播放动画
myAnimation.play()
// 两秒后调整动画速度
setTimeout(() => {
  myAnimation.steFrameSpeed(500)
  // 再两秒后暂停动画
  setTimeout(() => {
    myAnimation.stop()
  }, 2000)
}, 2000)

ts 类型说明

interface IMyRequestAnimationFrame {
  callback: () => number // 回调函数 返回值表示距离网页加载的时间,单位ms(requestAnimationFrame传入callback的参数)
  frameSpeed?: number // 触发间隔 ms
  config?: {
    autoPlay?: boolean // 【true】 是否自动播放
    now?: boolean // 【false】 是否立即执行一次
  }
}

参考链接

上次编辑于:
贡献者: yinan