requestIdleCallback 基本概念这里不讲,讲的文章很多。 本章主要参考走进React Fiber世界一文,里面举了一些例子,为了更好理解才有了本篇
一帧做什么
Inpute Events -> JS Timer -> Begin Frame(window resize/scroll/media query change) -> requestAnimationFrame -> Layout -> Paint -> requestIdleCallback
可以看到,requestIdleCallback是在一帧的最后的。它的意思是,在这一帧里(现代浏览器大概16.6ms刷新一帧),如果还有剩余的时间,那么就执行requestIdleCallback里的任务。如果没有时间了,那么就先不执行。
但requestIdleCallback也可以接受一个入参timeout,这就是告诉浏览器,如果到了这个时间,即使这一帧没有剩余时间了,你也要执行我这个任务。 MDN中的解释是:
如果指定了timeout,并且有一个正值,而回调在timeout毫秒过后还没有被调用,那么回调任务将放入事件循环中排队,即使这样做有可能对性能产生负面影响。
也就是说,从执行了 requestIdleCallback 函数起,超过 timeout 时间后,requestIdleCallback 传递的callback,还没有被调用,则强制将 callback 放入到任务队列
,而不是放入调用栈
调用。
window.requestIdleCallback(callback, { timeout, 1000})
一帧执行
javascript
let taskQueue = [
() => {
console.log('task1 start')
console.log('task1 end')
},
() => {
console.log('task2 start')
console.log('task2 end')
},
() => {
console.log('task3 start')
console.log('task3 end')
}
]
const performUnitWork = () => {
// 取出第一个队列中的第一个任务并执行
taskQueue.shift()()
}
// deadline是一个对象,有两个属性
// timeRemaining函数可返回此帧还有多少时间供用户使用
// didTimeout 此callback任务是否超时
const workloop = (deadline) => {
console.log(`此帧的剩余时间为: ${deadline.timeRemaining()}`)
// 如果此帧剩余时间大于0或者已经到了定义的超时时间,且当时存在任务,则直接执行这个任务
// 如果没有剩余时间,则应该放弃执行任务控制权,把执行权交还给浏览器
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && taskQueue.length > 0) {
performUnitWork()
}
// 如果还有未完成的任务,继续调用requestIdleCallback申请下一个时间片
if (taskQueue.length > 0) {
window.requestIdleCallback(workloop)
}
}
requestIdleCallback(workloop)
- 输出
几乎同时
,输出了下列内容
markdown
> 此帧的剩余时间为: 11.7
> task1 start
> task1 end
> task2 start
> task2 end
> task3 start
> task3 end
- 分析
原因在于,console.log 执行非常快,deadline.timeRemaining()
一直大于0,也就是一直有空余时间,所以在一帧渲染空余时间里,任务都执行完成了
多帧执行
在task1、task2、task3中加入睡眠时间,各自执行时间超过16ms:
javascript
const sleep = delay => {
for (let start = Date.now(); Date.now() - start <= delay;) {}
}
let taskQueue = [
() => {
console.log('task1 start')
sleep(20) // 已经超过一帧的时间(16.6ms),需要把控制权交给浏览器
console.log('task1 end')
},
() => {
console.log('task2 start')
sleep(20)
console.log('task2 end')
},
() => {
console.log('task3 start')
sleep(20)
console.log('task3 end')
}
]
const performUnitWork = () => {
taskQueue.shift()()
}
const workloop = (deadline) => {
console.log(`此帧的剩余时间为: ${deadline.timeRemaining()}`)
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && taskQueue.length > 0) {
performUnitWork()
}
if (taskQueue.length > 0) {
window.requestIdleCallback(workloop)
}
}
requestIdleCallback(workloop)
- 输出
此时从视觉上看,感觉稍微慢了一点点,有种连贯输出内容的感觉,但不是一次性输出的。而实际上,这里也是3帧输出以下内容
markdown
> 此帧的剩余时间为: 14.2
> task1 start
> task1 end
> 此帧的剩余时间为: 1.1
> task2 start
> task2 end
> 此帧的剩余时间为: 9
> task3 start
> task3 end
- 分析
- 首次进来,该帧还有"14.2ms",开始执行第一个任务。但该任务执行完后,发现该帧已经不剩时间了,把下个任务推到 idleCallback 里,然后把控制权还给浏览器
- 第二帧开始,执行后还剩下"1.1ms",开始执行 idleCallback,也就是第二个任务。同上,归还控制权
- 第三帧开始,执行后还剩下"9ms",同上
- 疑惑
我在这里就有两个疑惑,一是为什么每帧剩余的时间不一样,我执行的代码都差不多呀?二是这个console
究竟是什么时候绘制的
重复上述代码
在多执行几次后,竟然概率性输出下面内容:
markdown
> 此帧的剩余时间为: 9.7
> task1 start
> task1 end
> 此帧的剩余时间为: 15.4
> task2 start
> task2 end
> 此帧的剩余时间为: 49.1
> task3 start
> task3 end
剩余时间竟然"49.1ms",说好的16ms一帧呢?后来发现,浏览器一帧的时间并不严格是16ms,是可以动态控制的
,但为什么会偶现这么大的数值,我至今不太清楚
增大单个任务时长
javascript
const sleep = delay => {
for (let start = Date.now(); Date.now() - start <= delay;) { }
}
let taskQueue = [
() => {
console.log('task1 start')
sleep(2000)
console.log('task1 end')
},
() => {
console.log('task2 start')
sleep(2000)
console.log('task2 end')
},
() => {
console.log('task3 start')
sleep(2000)
console.log('task3 end')
}
]
const performUnitWork = () => {
taskQueue.shift()()
}
const workloop = (deadline) => {
console.log(`此帧的剩余时间为: ${deadline.timeRemaining()}`)
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && taskQueue.length > 0) {
performUnitWork()
}
if (taskQueue.length > 0) {
window.requestIdleCallback(workloop);
}
}
requestIdleCallback(workloop)
- 输出
markdown
// 停顿2s(已经在执行第一个idle任务中)
> 此帧的剩余时间为: 14.6(这时候才打印第一个idle任务执行前的从搜了)
> task1 start
> task1 end
// 停顿2s
> 此帧的剩余时间为: 6.6
> task2 start
> task2 end
//停顿2s
> 此帧的剩余时间为: 1.5
> task3 start
> task3 end
- 分析 如果我们将
sleep
改为10s,那对应的整个过程就持续30s(差不多)
但有一个现象是,并不是完整的按照10s 10s 10s
的停顿打印,可能会出现20s 10s
这种打印。仿佛将控制权交给浏览器后,浏览器还没来得及输出,就进行了下一次IdleCallback
那这个10s后交出控制权的意义在哪儿呢?既然打印都如此不遵守时间的话
答案是在事件响应、绘制等任务上。也就是说,在这30s期间,你尝试滚动鼠标(或者输出等其他事件),只有中间的两次交出控制权的间隙是成功滚动的,其他时间滚动都是无效的
另外通过这个例子我们可以看到,一旦idle任务开始执行,这个任务如果时间过长,超过了一帧的时间,由于此时主动权没有交还给浏览器,所以浏览器没办法进行渲染、响应输入等其他动作,此时就会出现"卡顿"现象