一般的浏览器每秒钟会绘制60帧,也就是每帧需要16ms左右,如果js计算任务长时间占用线程那个,会导致一些ui无法及时得到渲染,出现卡顿。
一帧内需要完成如下六个步骤的任务:
- 处理用户的交互
- JS 解析执行
- 帧开始。窗口尺寸变更,页面滚去等的处理
- requestAnimationFrame(rAF)
- 布局
- 绘制
上面六个步骤完成后没超过 16 ms,说明时间有富余,此时就会执行 requestIdleCallback
里注册的任务。
语法:
var handle = window.requestIdleCallback(callback[, options])
- callback:回调,即空闲时需要执行的任务,该回调函数接收一个
IdleDeadline
对象作为入参。其中IdleDeadline
对象包含:didTimeout
,布尔值,表示任务是否超时,结合timeRemaining
使用。timeRemaining()
,表示当前帧剩余的时间,也可理解为留给任务的时间还有多少。
- options:目前 options 只有一个参数
timeout
。表示超过这个时间后,如果任务还没执行,则强制执行,不必等待空闲。
实例:下面的例子知识模拟情况对比,实际情况下尽量避免在requestIdlecallback中操作dom.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>requestIdelCallback</title>
</head>
<body>
<div id ="ui">0</div>
<div>执行<span id="test">0</span></div>
<button onclick = "mytest()">常规执行计算任务</button>
<button onclick = "startRequestIdleCallback()">在requestIdleCallback中执行计算任务</button>
<script>
// 任务队列
let task1 = [
function(){
for(var i=0;i<3000;i++){
console.log(i)
}
console.log("第一个任务");
document.getElementById('test').innerHTML = 1
},
function(){
for(var i=0;i<3000;i++){
console.log(i)
}
console.log("第二个任务");
document.getElementById('test').innerHTML = 2
},
function(){
for(var i=0;i<3000;i++){
console.log(i)
}
console.log("第三个任务");
document.getElementById('test').innerHTML = 3
},
];
let task2 = [
function(){
for(var i=0;i<3000;i++){
console.log(i)
}
console.log("第一个任务");
document.getElementById('test').innerHTML = 1
},
function(){
for(var i=0;i<3000;i++){
console.log(i)
}
console.log("第二个任务");
document.getElementById('test').innerHTML = 2
},
function(){
for(var i=0;i<3000;i++){
console.log(i)
}
console.log("第三个任务");
document.getElementById('test').innerHTML = 3
},
];
setInterval(() => {
document.getElementById('ui').innerHTML = parseInt(document.getElementById('ui').innerHTML) + 1
},100)
//执行mytest文件会影响上面动态dom的渲染
function mytest(){
if (task1.length > 0){
work1(task1)
}
}
//将js计算任务放在requestIdleCallback中运算
function startRequestIdleCallback(){
requestIdleCallback(myNonEssentialWork, { timeout: 2000 });
}
function myNonEssentialWork (deadline) {
//alert(deadline.timeRemaining())
console.log(deadline,deadline.timeRemaining())
// 如果帧内有富余的时间,或者超时
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && task2.length > 0) {
work2(task2);
}
//开启下一个空闲时间的回调函数
if (task2.length > 0){requestIdleCallback(myNonEssentialWork);}
}
//
function work1 (tasks) {
tasks.shift()();
console.log('执行任务');
if(task1.length > 0){
work1(task1)
}
}
function work2 (tasks) {
tasks.shift()();
console.log('执行任务');
}
</script>
</body>
</html>
通过上面的例子可以得到:
1,直接执行任务队列,会导致#test元素的文本无法及时渲染且 定时渲染任务会出现长时间卡顿
2,在requestIdleCallback中执行任务队列,是在空闲时间去执行队列中的任务,#test的文本内容可以得到及时渲染并且定时任务卡顿不那么严重
3,虽然requestIdleCallback是在空闲时间去执行队列中的任务,但如果某个回调任务的计算耗时过长,也会导致当前帧的延长,出现卡顿
4,所以要把任务分成更小的任务单元,在浏览器空余时间执行,多次往返循环
总结
一些低优先级的任务可使用 requestIdleCallback
等浏览器不忙的时候来执行,同时因为时间有限,它所执行的任务应该尽量是能够量化,细分的微任务(micro task)
因为它发生在一帧的最后,此时页面布局已经完成,所以不建议在 requestIdleCallback
里再操作 DOM,这样会导致页面再次重绘。DOM 操作建议在 rAF 中进行。同时,操作 DOM 所需要的耗时是不确定的,因为会导致重新计算布局和视图的绘制,所以这类操作不具备可预测性。
Promise 也不建议在这里面进行,因为 Promise 的回调属性 Event loop 中优先级较高的一种微任务,会在 requestIdleCallback
结束时立即执行,不管此时是否还有富余的时间,这样有很大可能会让一帧超过 16 ms。
https://www.jianshu.com/p/2771cb695c81