为什么要手写requestIdleCallback
requestIdleCallback兼容性
从上图可以看出手写的原因是:requestIdleCallback兼容性并不好
requestIdleCallback是用来干什么的呢?
是一个当浏览器处于闲置状态时,调度工作的新的性能相关的API
requestIdleCallback
通过上图可看到,一帧内需要完成如下六个步骤的任务:
- 处理用户的交互
- JS 解析执行
- 帧开始。窗口尺寸变更,页面滚去等的处理
- requestAnimationFrame(rAF)
- 布局
- 绘制
requestIdleCallback
上面六个步骤完成后没超过 16 ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务。
图一
从上图也可看出,requestIdleCallback 是捡浏览器空闲来执行任务。
如此一来,假如浏览器一直处于非常忙碌的状态,requestIdleCallback 注册的任务有可能永远不会执行。此时可通过设置 timeout (见下面 API 介绍)来保证执行。
总结:
方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。
下面我们将基于 MessageChannel 、requestAnimationFrame手写requestIdleCallback
要想通透的学会(必须将代码复制下来手一遍,如果看不懂注释留言评论)
// 简单睡眠
function sleep(duration) {
let start = Date.now();
while (start + duration > Date.now()) { }
}
const works = [
() => {
console.log('A1开始');
sleep(20);
console.log('A1结束');
},
() => {
console.log('A2开始');
sleep(0);
console.log('A2结束');
},
() => {
console.log('A3开始');
sleep(0);
console.log('A3结束');
},
() => {
console.log('A4开始');
sleep(0);
console.log('A4结束');
},
() => {
console.log('A4开始');
sleep(0);
console.log('A4结束');
}
]
let channle = new MessageChannel();
var activeFrameTime = 1000 / 60; // 16.6 表示每帧
let frameDeadline; // 这一帧的截至时间
let pendingCallback; // 此变量用来保存即将执行的函数
let timeRemaining = _ => frameDeadline - performance.now();
// 监听channle.port1.postMessage 消息
channle.port2.onmessage = function () {
let currentTime = performance.now();
//如果帧的截止时间已经小于当前时间,说明已经过期了
let didTimeout = frameDeadline <= currentTime;
if (didTimeout || timeRemaining() > 0) {
if (pendingCallback)
pendingCallback({ didTimeout, timeRemaining });
}
}
// 注册requestIdleCallback
window.requestIdleCallback = (callback, options) => {
/**
* rafTime —— 该参数与performance.now()的返回值相同
*/
requestAnimationFrame(rafTime => {
//每一帧开始的时间加上16.6=就是一帧的截止时间了
frameDeadline = rafTime + activeFrameTime;
pendingCallback = callback;
//其实发消息之后相当于添加一个宏任务
channle.port1.postMessage('hello');
})
}
//告诉浏览器,你可以在空闲的时间执行任务,但是如果已经过期了,不管你有没有空,都要帮我执行
requestIdleCallback(workLoop, { timeout: 1000 });
/**
* 循环执行工具
* @params {Object} deadline
* deadline —— {didTimeout,timeRemaining}
* didTimeout —— 是否到了将控制权归还给浏览器的时间
* timeRemaining() 本帧的剩余时间
*/
function workLoop(deadline) {
console.log('本帧的剩余时间', parseInt(deadline.timeRemaining()));
//如果说还有剩余时间或者此任务已经过期了 并且还有没有完成的任务
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && works.length > 0) {
performUnitOfWork();
}
if (works.length > 0) {
console.log(`只剩下${deadline.timeRemaining()},时间片已经到期了,等待下次调度`);
requestIdleCallback(workLoop);
}
}
//取出工作数组中的第一个工作并执行
function performUnitOfWork() {
works.shift()(); // 等价于 let work = works.shift(); work();
}