React Scheduler 源码详解(2)

上一篇

React Scheduler 源码详解(1)浏览器

上次讲述了任务的优先级,以及如何根据优先级(过时时间)加入任务链表,今天来分析一下如何在一个合适的时机去执行任务。bash

1 requestIdleCallback pollyfill

上文讲到要用requetAnimationFrame去模拟requestIdleCallback,但requetAnimationFrame有个缺点,就是当前tab若是处于不激活状态的话,requestAnimationFrame是不工做的,因此须要requestAnimationFramesetTimeout联合起来保证任务的执行。这就是上文末讲到的requestAnimationFrameWithTimeout的做用,当前tab处于激活状态时,至关于requestAnimationFrame在调度任务,当前tab切到未激活时setTimeout接管任务执行。为了理解方便,下文咱们就用requestAnimationFrame来表示requestAnimationFrameWithTimeout函数

0.流程

咱们先来描述一下整个的执行流程,在每一帧开始的rAF的回调里记录每一帧的开始时间,并计算每一帧的过时时间,而后经过messageChannel发送消息。在帧末messageChannel的回调里接收消息,根据当前帧的过时时间和当前时间进行比对来决定当前帧可否执行任务,若是能的话会依次从任务链表里拿出队首任务来执行,执行尽量多的任务后若是还有任务,下一帧再从新调度。post

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。ui

3.建立一个消息信道

var channel = new MessageChannel();
     var port = channel.port2; //port2用来发消息
     channel.port1.onmessage = function(event) {
        //port1监听消息的回调来作任务调度的具体工做,后面再说
        //onmessage的回调函数的调用时机是在一帧的paint完成以后,因此适合作一些重型任务,也能保证页面流畅不卡顿
     }
复制代码

4.执行任务

下面就在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)
     }
复制代码

5.执行器

上文提到的执行器 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,用来作防止重入、防护判断等,并考虑了任务执行过程当中有新的任务不断加入等场景的逻辑。这一块须要感兴趣的读者自行去体会了。

2 总结

最后在描述一下总体的任务调度流程

  • 一、任务根据优先级和加入时的当前时间来肯定过时时间
  • 二、任务根据过时时间加入任务链表
  • 三、任务链表有两种状况会启动任务的调度,1是任务链表从无到有时,2是任务链表加入了新的最高优先级任务时。
  • 四、任务调度指的是在合适的时机去执行任务,这里经过requestAnimationFramemessageChannel来模拟
  • 五、requestAnimationFrame回调在帧首执行,用来计算当前帧的截止时间并开启递归,messageChannel的回调在帧末执行,根据当前帧的截止时间、当前时间、任务链表第一个任务的过时时间来决定当前帧是否执行任务(或是到下一帧执行)
  • 六、若是执行任务,则根据任务是否过时来肯定如何执行任务。任务过时的话就会把任务链表内过时的任务都执行一遍直到没有过时任务或者没有任务;任务没过时的话,则会在当前帧过时以前尽量多的执行任务。最后若是还有任务,则回到第5步,放到下一帧再从新走流程。
相关文章
相关标签/搜索