# RAF替代setTimeout_setInterval

# 前言

为什么用 requestAnimationFrame代替 setIntervalsetTimeout?那得先了解微任务、宏任务与Event-Loop, 还有 setInterval 存在的问题

# requestAnimationFrame

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画, 并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。 该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用window.requestAnimationFrame()

# 语法

window.requestAnimationFrame(callback);

  • callback 下一次重绘之前更新动画帧所调用的函数(即上面所说的回调函数)。该回调函数会被传 入DOMHighResTimeStamp参数,该参数与performance.now()的返回值相同,它表示requestAnimationFrame() 开 始去执行回调函数的时刻。
  • 返回值 一个 long 整数,请求 ID ,是回调列表中唯一的标识。是个非零值,没别的意义。你可以传这个值给 window.cancelAnimationFrame() 以取消回调函数。

# 了解微任务、宏任务与Event-Loop

微任务_宏任务与Event-Loop

# 运行机制

# setTimeout

setTimeout(function(){
    console.log(1);
});
console.log(0);

1
2
3
4
5

实际上,把setTimeout的第二个参数设置为0s,并不是立即执行函数的意思,只是把函数放入异步队列。浏览器先执行完同步队列里的任务,才会去执行异步队列中的任务

在下面这个例子中,给一个按钮btn设置了一个事件处理程序。事件处理程序设置了一个250ms后调用的定时器。点击该按钮后, 首先将onclick事件处理程序加入队列。该程序执行后才设置定时器,再有250ms后,指定的代码才被添加到队列中等待执行


btn.onclick = function(){
    setTimeout(function(){
        console.log(1);
    },250);
}
1
2
3
4
5
6

如果上面代码中的onclick事件处理程序执行了300ms,那么定时器的代码至少要在定时器设置之后的300ms后才会被执行。队列中所有 的代码都要等到javascript进程空闲之后才能执行,而不管它们是如何添加到队列中的

# setInterval

使用setInterval()的问题在于,定时器代码可能在代码再次被添加到队列之前还没有完成执行,结果导致定时器代码连续运行好几次,而之间没有任何停顿。 而javascript引擎对这个问题的解决是:当使用setInterval()时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。这确保了定时器代码 加入到队列中的最小时间间隔为指定间隔

但是,这样会导致两个问题:1、某些间隔被跳过;2、多个定时器的代码执行之间的间隔可能比预期的小

假设,某个onclick事件处理程序使用setInterval()设置了200ms间隔的定时器。如果事件处理程序花了300ms多一点时间完成,同时定时器代码也花了差不 多的时间,就会同时出现跳过某间隔的情况

例子中的第一个定时器是在205ms处添加到队列中的,但是直到过了300ms处才能执行。当执行这个定时器代码时,在405ms处又给队列 添加了另一个副本。在下一个间隔,即605ms处,第一个定时器代码仍在运行,同时在队列中已经有了一个定时 器代码的实例。结果是,在这个时间点上的定时器代码不会被添加到队列中

# 迭代setTimeout实现setInterval

某些公司会禁用 setInterval 为了避免setInterval()定时器的问题,可以使用链式setTimeout()调用

setTimeout(function fn(){
    setTimeout(fn,interval);
},interval);
1
2
3

# 模拟setInterval/setTimeout的基本使用

# RAF

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])
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 使用

var raf = new RAF()
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)
1
2
3
4
5
6
7
8
9
10
11
12
13

# 计算计算机刷新频率

 // 题外计算一下 requestAnimationFrame 刷新频率
  let startTime = Date.now()
  let timer = null
  let arr = []
  function calc () {
    timer = requestAnimationFrame(calc)
    const nowTime = Date.now()
    const diff = nowTime - startTime
    startTime = nowTime
    arr.push(diff)
  }
  calc()
  setTimeout(() => {
    const newArr = arr.filter(v => v > 10)
    console.log(newArr)
    const total = newArr.reduce((t, i) => t + i, 0)
    console.log('平均刷新次数', 1000 / (total / newArr.length))
    cancelAnimationFrame(timer)
  }, 3000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 引用