上次讲述了任务的优先级,以及如何根据优先级(过时时间)加入任务链表,今天来分析一下如何在一个合适的时机去执行任务。bash
requestIdleCallback pollyfill
上文讲到要用requetAnimationFrame
去模拟requestIdleCallback
,但requetAnimationFrame
有个缺点,就是当前tab
若是处于不激活状态的话,requestAnimationFrame
是不工做的,因此须要requestAnimationFrame
和setTimeout
联合起来保证任务的执行。这就是上文末讲到的requestAnimationFrameWithTimeout
的做用,当前tab处于激活状态时,至关于requestAnimationFrame
在调度任务,当前tab切到未激活时setTimeout
接管任务执行。为了理解方便,下文咱们就用requestAnimationFrame
来表示requestAnimationFrameWithTimeout
。函数
咱们先来描述一下整个的执行流程,在每一帧开始的rAF的回调里记录每一帧的开始时间,并计算每一帧的过时时间,而后经过messageChannel发送消息。在帧末messageChannel的回调里接收消息,根据当前帧的过时时间和当前时间进行比对来决定当前帧可否执行任务,若是能的话会依次从任务链表里拿出队首任务来执行,执行尽量多的任务后若是还有任务,下一帧再从新调度。post
var scheduledHostCallback = null; //表明任务链表的执行器
var timeoutTime = -1; //表明最高优先级任务firstCallbackNode的过时时间
var activeFrameTime = 33; // 一帧的渲染时间33ms,这里假设 1s 30帧
var frameDeadline = 0; //表明一帧的过时时间,经过rAF回调入参t加上activeFrameTime来计算
复制代码
首先咱们先利用requestAnimationFrame
来计算每一帧的截止时间优化
// rAF的回调是每一帧开始的时候,因此适合作一些轻量任务,否则会阻塞渲染。
function animationTick(rafTime) {
// 有任务再进行递归,没任务的话不须要工做
if (scheduledHostCallback !== null) {
requestAnimationFrame(animationTick)
}
//计算当前帧的截止时间,用开始时间加上每一帧的渲染时间
frameDeadline = rafTime + activeFrameTime;
}
//某个地方会调用
requestAnimationFrame(animationTick)
复制代码
源码里有对每一帧渲染时间的一个优化过程,会在渲染过程当中不断压缩每一帧的渲染时间,达到系统的刷新频率(60hz为16.6ms)。由于不是重点就先略过了,这里假设就是33ms。ui
var channel = new MessageChannel();
var port = channel.port2; //port2用来发消息
channel.port1.onmessage = function(event) {
//port1监听消息的回调来作任务调度的具体工做,后面再说
//onmessage的回调函数的调用时机是在一帧的paint完成以后,因此适合作一些重型任务,也能保证页面流畅不卡顿
}
复制代码
下面就在animationTick
里向channel
发消息,而后在port1
的回调里去决定当前帧要不要执行任务,执行多少任务等问题。spa
function animationTick(rafTime) {
// 有任务再进行递归,没任务的话不须要工做
if (scheduledHostCallback !== null) {
requestAnimationFrame(animationTick)
}
//计算当前帧的截止时间,用开始时间加上每一帧的渲染时间
frameDeadline = rafTime + activeFrameTime;
//新加的代码,在当前帧结束去搞一些事情
port.postMessage(undefined);
}
//仔细看这段注释
//下面的代码逻辑决定当前帧要不要执行任务
// 一、若是当前帧没过时,说明当前帧有富余时间,能够执行任务
// 二、若是当前帧过时了,说明当前帧没有时间了,这里再看一下当前任务firstCallbackNode是否过时,若是过时了也要执行任务;若是当前任务没过时,说明不着急,那就先不执行去下一帧再说。
channel.port1.onmessage = function(event) {
var currentTime = getCurrentTime(); //获取当前时间,
var didTimeout = false; //是否过时
if (frameDeadline - currentTime <= 0) { // 当前帧过时
if (timeoutTime <= currentTime) {
// 当前任务过时
// timeoutTime 为当前任务的过时时间,会有个地方赋值。
didTimeout = true;
} else {
//当前帧因为浏览器渲染等缘由过时了,那就去下一帧再处理
return;
}
}
// 到了这里有两种状况,1是当前帧没过时;2是当前帧过时且当前任务过时,也就是上面第二个if里的逻辑。下面就是要调用执行器,依次执行链表里的任务
scheduledHostCallback(didTimeout)
}
复制代码
上文提到的执行器 scheduledHostCallback
也就是下面的flushWork
,flushWork
根据didTimeout
参数有两种处理逻辑,若是为true
,就会把任务链表里的过时任务全都给执行一遍;若是为false
则在当前帧到期以前尽量多的去执行任务。code
function flushWork(didTimeout) {
if (didTimeout) { //任务过时
while (firstCallbackNode !== null) {
var currentTime = getCurrentTime(); //获取当前时间
if (firstCallbackNode.expirationTime <= currentTime) {//若是队首任务时间比当前时间小,说明过时了
do {
flushFirstCallback(); //执行队首任务,把队首任务从链表移除,并把第二个任务置为队首任务。执行任务可能产生新的任务,再把新任务插入到任务链表
} while (
firstCallbackNode !== null &&
firstCallbackNode.expirationTime <= currentTime
);
continue;
}
break;
}
}else{
//下面再说
}
}
复制代码
注意,上面有两重while
循环,外层的while
循环每次都会获取当前时间,内层循环根据这个当前时间去判断任务是否过时并执行。这样当内层执行了若干任务后,当前时间又会向前推动一块。外层循环再从新获取当前时间,直到没有任务过时或者没有任务为止。递归
下面看一下没有过时的处理状况get
function flushWork(didTimeout) {
if (didTimeout) { //任务过时
...
}else{
//当前帧有富余时间,while的逻辑是只要有任务且当前帧没过时就去执行任务。
if (firstCallbackNode !== null) {
do {
flushFirstCallback();//执行队首任务,把队首任务从链表移除,并把第二个任务置为队首任务。执行任务可能产生新的任务,再把新任务插入到任务链表
} while (firstCallbackNode !== null && !shouldYieldToHost());
}
}
}
复制代码
上面的shouldYieldToHost
表明当前帧过时了,取反的话就是没过时。每次while
都会执行这个判断。
shouldYieldToHost = function() {
// 当前帧的截止时间比当前时间小则为true,表明当前帧过时了
return frameDeadline <= getCurrentTime();
};
复制代码
下面继续看flushWork
function flushWork(didTimeout) {
if (didTimeout) { //任务过时
...
}else{ //当前帧有富余时间
...
}
//最后,若是还有任务的话,再启动一轮新的任务执行调度
if (firstCallbackNode !== null) {
ensureHostCallbackIsScheduled();
}
//最最后,若是还有任务且有最高优先级的任务,就都执行一遍。
flushImmediateWork();
}
复制代码
本文讲的比较简略,源码中有大量flag
,用来作防止重入、防护判断等,并考虑了任务执行过程当中有新的任务不断加入等场景的逻辑。这一块须要感兴趣的读者自行去体会了。
最后在描述一下总体的任务调度流程
requestAnimationFrame
和messageChannel
来模拟requestAnimationFrame
回调在帧首执行,用来计算当前帧的截止时间并开启递归,messageChannel
的回调在帧末执行,根据当前帧的截止时间、当前时间、任务链表第一个任务的过时时间来决定当前帧是否执行任务(或是到下一帧执行)