为什么用 requestAnimationFrame代替 setInterval 和 setTimeout?那得先了解微任务、宏任务与Event-Loop, 还有 setInterval 存在的问题
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,
并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。 该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用window.requestAnimationFrame()
window.requestAnimationFrame(callback);
微任务_宏任务与Event-Loop
setTimeout(function(){
console.log(1);
});
console.log(0);
实际上,把setTimeout的第二个参数设置为0s,并不是立即执行函数的意思,只是把函数放入异步队列。浏览器先执行完同步队列里的任务,才会去执行异步队列中的任务
在下面这个例子中,给一个按钮btn设置了一个事件处理程序。事件处理程序设置了一个250ms后调用的定时器。点击该按钮后,
首先将onclick事件处理程序加入队列。该程序执行后才设置定时器,再有250ms后,指定的代码才被添加到队列中等待执行
btn.onclick = function(){
setTimeout(function(){
console.log(1);
},250);
}
如果上面代码中的onclick事件处理程序执行了300ms,那么定时器的代码至少要在定时器设置之后的300ms后才会被执行。队列中所有
的代码都要等到javascript进程空闲之后才能执行,而不管它们是如何添加到队列中的
使用setInterval()的问题在于,定时器代码可能在代码再次被添加到队列之前还没有完成执行,结果导致定时器代码连续运行好几次,而之间没有任何停顿。
而javascript引擎对这个问题的解决是:当使用setInterval()时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。这确保了定时器代码
加入到队列中的最小时间间隔为指定间隔
但是,这样会导致两个问题**:1、某些间隔被跳过;2、多个定时器的代码执行之间的间隔可能比预期的小**
假设,某个onclick事件处理程序使用setInterval()设置了200ms间隔的定时器。如果事件处理程序花了300ms多一点时间完成,同时定时器代码也花了差不
多的时间,就会同时出现跳过某间隔的情况
例子中的第一个定时器是在205ms处添加到队列中的,但是直到过了300ms处才能执行。当执行这个定时器代码时,在405ms处又给队列
添加了另一个副本。在下一个间隔,即605ms处,第一个定时器代码仍在运行,同时在队列中已经有了一个定时
器代码的实例。结果是,在这个时间点上的定时器代码不会被添加到队列中
某些公司会禁用 setInterval,为了避免setInterval()定时器的问题,可以使用链式setTimeout()调用
setTimeout(function fn(){
setTimeout(fn,interval);
},interval);
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])
}
}
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)
// 题外计算一下 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)) // 平均刷新次数 60.00670465973852
cancelAnimationFrame(timer)
}, 3000)
requestAnimationFrame
深入理解定时器系列第一篇——理解setTimeout和setInterval
RAF替代setTimeout/setInterval