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步,放到下一幀再從新走流程。
相關文章
相關標籤/搜索