首发于前端杂谈

RAF替代setTimeout/setInterval

requestAnimationFrame相较于setTimeout、setInterval的优点这里不多说,想要了解的同学可以戳requestAnimationFrame最佳实践

requestAnimationFrame原生没有自定义时间间隔执行的功能,比如想要实现每隔1s执行一次的功能或者延时1s执行,每次都要重写间隔时间的重复代码,想着可以将之抽出。直接上代码:

const RAF = {
  intervalTimer: null,
  timeoutTimer: null,
  setTimeout (cb, interval) { // 实现setTimeout功能
    let now = Date.now
    let stime = now()
    let etime = stime
    let loop = () => {
      this.timeoutTimer = requestAnimationFrame(loop)
      etime = now()
      if (etime - stime >= interval) {
        cb()
        cancelAnimationFrame(this.timeoutTimer)
      }
    }
    this.timeoutTimer = requestAnimationFrame(loop)
    return this.timeoutTimer
  },
  clearTimeout () {
    cancelAnimationFrame(this.timeoutTimer)
  },
  setInterval (cb, interval) { // 实现setInterval功能
    let now = Date.now
    let stime = now()
    let etime = stime
    let loop = () => {
      this.intervalTimer = requestAnimationFrame(loop)
      etime = now()
      if (etime - stime >= interval) {
        stime = now()
        etime = stime
        cb()
      }
    }
    this.intervalTimer = requestAnimationFrame(loop)
    return this.intervalTimer
  },
  clearInterval () {
    cancelAnimationFrame(this.intervalTimer)
  }
}

进行简单测试:

let count = 0
function a() {
  console.log(count)
  count++
}
RAF.setInterval(a, 1000)

这里没有实现setTimeout、setInterval的返回值功能,不过返回值功能大多用在清除定时器上,目前提供了clearTimeout和clearInterval的方法,所以返回值可以不必返回



2018.7.6更新。

其实上面这种有很大的bug,在调用多次时会出现清除不了循环或者定时器的问题。

RAF.setInterval(() => {
  console.log(1000)   
}, 1000)
RAF.setInterval(() => {
  console.log(1500)   
}, 1500)


这是当初选用intervalTimer直接作为基本类型来管理timer,后面的setInterval生成的intervalTimer值覆盖掉了前面的那个,继续修改:

class RAF {
  constructor () {
    this.init()
  }
  init () {
    this._timerIdMap = {
      timeout: {},
      interval: {}
    }
  }
  run (type = 'interval', cb, interval = 16.7) {
    const now = Date.now
    let stime = now()
    let etime = stime
    //创建Symbol类型作为key值,保证返回值的唯一性,用于清除定时器使用
    const timerSymbol = Symbol()
    const loop = () => {
      this.setIdMap(timerSymbol, type, loop)
      etime = now()
      if (etime - stime >= interval) {
        if (type === 'interval') {
          stime = now()
          etime = stime
        }
        cb()
        type === 'timeout' && this.clearTimeout(timerSymbol)
      }
    }
    this.setIdMap(timerSymbol, type, loop)
    return timerSymbol // 返回Symbol保证每次调用setTimeout/setInterval返回值的唯一性
  }
  setIdMap (timerSymbol, type, loop) {
    const id = requestAnimationFrame(loop)
    this._timerIdMap[type][timerSymbol]= id
  }
  setTimeout (cb, interval) {  // 实现setTimeout 功能
    return this.run('timeout', cb, interval)
  }
  clearTimeout (timer) {
    cancelAnimationFrame(this._timerIdMap.timeout[timer])
  }
  setInterval (cb, interval) { // 实现setInterval功能
    return this.run('interval', cb, interval)
  }
  clearInterval (timer) {
    cancelAnimationFrame(this._timerIdMap.interval[timer])
  }
}

这次使用

this._timerIdMap = {
      timeout: {
        [timerSymbol]: id
      },
      interval: {
        [timerSymbol]: id
      }
    }

进行setTimeout/setInterval返回值timer的存储,timerSymbol(Symbol类型)作为key在调用setTimeout/setInterval时进行返回,这样就保证值得唯一性,在清除定时器时就不会发生混乱和覆盖了。测试:

var timer1 = raf.setInterval(() =>{
  console.log(1000)
}, 1000)

var timer2 = raf.setInterval(() =>{
  console.log(1500)
}, 1500)

raf.setTimeout(() => {
  raf.clearInterval(timer1)
  raf.clearInterval(timer2)
}, 6000)

测试结果:

至此实现了模拟setInterval/setTimeout的基本使用。

编辑于 2018-07-06

文章被以下专栏收录