内容简介:上次讲述了任务的优先级,以及如何根据优先级(过期时间)加入任务链表,今天来分析一下如何在一个合适的时机去执行任务。上文讲到要用
上次讲述了任务的优先级,以及如何根据优先级(过期时间)加入任务链表,今天来分析一下如何在一个合适的时机去执行任务。
1 requestIdleCallback pollyfill
上文讲到要用 requetAnimationFrame
去模拟 requestIdleCallback
,但 requestIdleCallback
有个缺点,就是当前 tab
如果处于不激活状态的话,requestAnimationFrame是不工作的,所以需要 requestAnimationFrame
和 setTimeout
联合起来保证任务的执行。这就是上文末讲到的 requestAnimationFrameWithTimeout
的作用,当前tab处于激活状态时,相当于 requestAnimationFrame
在调度任务,当前tab切到未激活时 setTimeout
接管任务执行。为了理解方便,下文我们就用 requestAnimationFrame
来表示 requestAnimationFrameWithTimeout
。
0.流程
我们先来描述一下整个的执行流程,在每一帧开始的rAF的回调里记录每一帧的开始时间,并计算每一帧的过期时间,然后通过messageChannel发送消息。在帧末messageChannel的回调里接收消息,根据当前帧的过期时间和当前时间进行比对来决定当前帧能否执行任务,如果能的话会依次从任务链表里拿出队首任务来执行,执行尽可能多的任务后如果还有任务,下一帧再重新调度。
1.声明变量
var scheduledHostCallback = null; //代表任务链表的执行器 var timeoutTime = -1; //代表最高优先级任务firstCallbackNode的过期时间 var activeFrameTime = 33; // 一帧的渲染时间33ms,这里假设 1s 30帧 var frameDeadline = 0; //代表一帧的过期时间,通过rAF回调入参t加上activeFrameTime来计算 复制代码
2.计算每一帧的截止时间
首先我们先利用 requestAnimationFrame
来计算每一帧的截止时间
// rAF的回调是每一帧开始的时候,所以适合做一些轻量任务,不然会阻塞渲染。 function animationTick(rafTime) { // 有任务再进行递归,没任务的话不需要工作 if (scheduledHostCallback !== null) { requestAnimationFrame(animationTick) } //计算当前帧的截止时间,用开始时间加上每一帧的渲染时间 frameDeadline = rafTime + activeFrameTime; } //某个地方会调用 requestAnimationFrame(animationTick) 复制代码
源码里有对每一帧渲染时间的一个优化过程,会在渲染过程中不断压缩每一帧的渲染时间,达到系统的刷新频率(60hz为16.6ms)。因为不是重点就先略过了,这里假设就是33ms。
3.创建一个消息信道
var channel = new MessageChannel(); var port = channel.port2; //port2用来发消息 channel.port1.onmessage = function(event) { //port1监听消息的回调来做任务调度的具体工作,后面再说 //onmessage的回调函数的调用时机是在一帧的paint完成之后,所以适合做一些重型任务,也能保证页面流畅不卡顿 } 复制代码
4.执行任务
下面就在 animationTick
里向 channel
发消息,然后在 port1
的回调里去决定当前帧要不要执行任务,执行多少任务等问题。
function animationTick(rafTime) { // 有任务再进行递归,没任务的话不需要工作 if (scheduledHostCallback !== null) { requestAnimationFrame(animationTick) } //计算当前帧的截止时间,用开始时间加上每一帧的渲染时间 frameDeadline = rafTime + activeFrameTime; //新加的代码,在当前帧结束去搞一些事情 port.postMessage(undefined); } //仔细看这段注释 //下面的代码逻辑决定当前帧要不要执行任务 // 1、如果当前帧没过期,说明当前帧有富余时间,可以执行任务 // 2、如果当前帧过期了,说明当前帧没有时间了,这里再看一下当前任务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) } 复制代码
5.执行器
上文提到的执行器 scheduledHostCallback
也就是下面的 flushWork
, flushWork
根据 didTimeout
参数有两种处理逻辑,如果为 true
,就会把任务链表里的过期任务全都给执行一遍;如果为 false
则在当前帧到期之前尽可能多的去执行任务。
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
循环每次都会获取当前时间,内层循环根据这个当前时间去判断任务是否过期并执行。这样当内层执行了若干任务后,当前时间又会向前推进一块。外层循环再重新获取当前时间,直到没有任务过期或者没有任务为止。
下面看一下没有过期的处理情况
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
,用来做防止重入、防御判断等,并考虑了任务执行过程中有新的任务不断加入等场景的逻辑。这一块需要感兴趣的读者自行去体会了。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- JDK SPI源码详解
- 【zookeeper源码】启动流程详解
- 详解RunLoop之源码分析
- 详解CopyOnWrite容器及其源码
- React Scheduler 源码详解(1)
- PHP文件上传原理详解(附源码)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。