React@16.8.6源碼閱讀(一)Scheduler

開篇

React也使用了好一段時間,最近纔有空把源碼閱讀一下(真是慚愧),由於項目用的React版本比較老,因此找的版本也就對應項目使用的版本恰好是16.8.6,不過React源碼分析的文章已經不少了,並且有不少也質量很高,因此這裏僅僅當作本身的筆記做爲記錄吧。react

問題

在還沒接觸React的時候就已經瞭解到React16的一些新特性:協程,分片等,那個時候恰好接觸到go語言對React的協程一直有幾個疑問,會不會跟go語言的協程同樣有調度器,React的調度單位是怎樣的,Fiber是怎樣搶佔,中斷和恢復執行的?git

Scheduler

React的調度器其實代碼量並很少,僅僅只有700多行,而後核心功能就是如下兩點:github

  1. 利用requestAnimationFrame來動態計算每幀的時間
  2. 利用MessageChannel來建立宏任務,來執行調度的任務

Scheduler目的是爲了讓任務都在每一幀的Idle階段來執行,利用的是每幀空閒時間,而不阻塞瀏覽器的佈局和繪製;
那麼爲何不在requestAnimationFrame階段來執行尼?咱們都知道raf會在瀏覽器佈局和繪製以前執行,但React是根本不知道瀏覽器接着後面佈局和繪製須要消耗多少時間,因此在raf階段處理是很難估計該預留多少時間本身去執行,而後讓回給瀏覽器。api

那麼爲何不使用requestIdleCallback來控制在每幀的Idle階段來執行尼?一開始React確實是這麼幹,可是後面由於requestIdleCallback的一些問題,並且新的api也有兼容性問題。瀏覽器

那麼如今的新辦法是如何處理的尼,首先用requestAnimationFrame先觸發一個anmiationTick,這裏有兩個做用:第一能夠預估每幀大概的時間;第二等anmiationTick觸發時再用postMessage觸發一個宏任務,這樣這個宏任務就會在瀏覽器的佈局和繪製以後執行,等同於在idle階段執行了,固然這個宏任務裏面還須要判斷當前幀時間是否沒有了(可是若是任務已經超時了無論還有沒有時間剩下也是會執行的),這個判斷就利用第一點獲取的幀時間來進行的,若是沒有剩餘時間了就再觸發一次animationTick,重複一次整個過程。異步

而每一個調度的任務都會帶有一個優先級priorityLevel,這個優先級是指:源碼分析

var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var LowPriority = 4;
var IdlePriority = 5;

這個優先級很重要,涉及到當前任務和這個任務執行時派生的任務的超時時間計算。佈局

另一些雜七雜八的點:post

  1. requestAnimationTime有個缺點就是頁面被隱藏的時候,有可能不執行,因此React採用了一個處理辦法:code

    var requestAnimationFrameWithTimeout = function(callback) {
      // schedule rAF and also a setTimeout
      rAFID = localRequestAnimationFrame(function(timestamp) {
     // cancel the setTimeout
     localClearTimeout(rAFTimeoutID);
     callback(timestamp);
      });
      rAFTimeoutID = localSetTimeout(function() {
     // cancel the requestAnimationFrame
     localCancelAnimationFrame(rAFID);
     callback(getCurrentTime());
      }, ANIMATION_FRAME_TIMEOUT);
    };

    利用setTimeout來兜底,這樣就萬無一失了。

  2. 而判斷任務是否要過時,就要不停使用peformance.now/Date.now來獲取當前時間,而獲取當前時間通常也是一個系統調用,頻繁調用也是一種消耗;因此會利用timeoutTime來記錄最近調度的一個任務的超時時間,執行的時候若是判斷已通過期,則認爲調度的任務列表裏面存在過時任務,先把全部的過時任務清理完,因此整個過程只須要獲取一次當前時間就能夠了,減小獲取當前時間的消耗。

與React結合

React的Scheduler算是一個獨立的包,徹底沒有包含React其餘內容,因此也很難回答我開頭的疑問,究竟它的調度單位是什麼,Fiber是怎樣搶佔和恢復的。
直接來到ReactFiberScheduler.js,scheduleWork方法:

function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
  const root = scheduleWorkToRoot(fiber, expirationTime); // 1
  if (
    !isWorking &&
    nextRenderExpirationTime !== NoWork &&
    expirationTime > nextRenderExpirationTime
  ) {
    resetStack(); // 2
  }
  markPendingPriorityLevel(root, expirationTime); // 3
  if (
    !isWorking ||
    isCommitting ||
    nextRoot !== root
  ) { // 4
    const rootExpirationTime = root.expirationTime;
    requestWork(root, rootExpirationTime);
  }
}

分四步來分析這個方法:

  1. 首先更新current樹和workInProgress(若是存在)樹fiber節點的childExpirationTime,而後返回root;這裏簡單說明一下,current和workInProgress樹,current樹就表明着當前顯示在用戶面前的fiber節點樹,workInProgress樹就是正在作更新的fiber節點樹,畢竟如今的React是diff過程已經能夠異步的,若是隻有一棵樹,那就頗有可能出現更新到一半就顯示給用戶了,綜合起來這種也算是遊戲中經常使用的雙緩衝技術的應用;而childExpirationTime表明的是子級節點中最高的優先級,能夠用在後面更新的時候快速判斷子級節點需不須要更新,由於每次調度更新的時候,都是從ReactFiberRoot往下遍歷,因此這個屬性就很重要了,能夠提升效率。
  2. 若是不在更新過程當中,出現了一種優先級更高的更新任務,也就是搶佔,這個時候會重置執行棧,以前更新到一半的工做結果都會被拋棄,等下次調度從新開始。
  3. 標記ReactFiberRoot的優先級,在我一開始的源碼閱讀中,我一開始簡單認爲expirationTime就是超時時間,實際上還包含優先級的意思,並且源碼中更多時候表明的是優先級,越往前調度的任務優先級越高,越日後就越低,高於當前的幀的deadline,都表示這些任務是過時任務,過時任務哪怕當前幀時間不夠都會所有調度執行。而ReactFiberRoot上會有好幾個字段跟優先級相關:

    earliestPendingTime
    latestPendingTime
    
    earliestSuspendedTime
    latestSuspendedTime
    
    latestPingedTime
    
    nextExpirationTimeToWorkOn
    expirationTime

    開頭那5兄弟一開始真的讓我感受有點懵逼,一開始徹底不知道爲何須要5個字段來標記優先級,在我認知裏面每一個節點僅僅須要一個expirationTime標記自身的優先級和childExpirationTime標記子級最高的優先級就足夠了;可是後面多閱讀幾遍代碼就發現它的意圖,在這些優先級裏面也是有分類的:Pending > Pinged > Suspended;React老是會先把Pending優先級任務清理完纔會清理後面的任務,而Pending優先級表明的是尚未執行過的任務。
    而nextExpirationTimeToWorkOn和expirationTime通常狀況下它們是相等,可是還有其餘狀況是不同(就是處理Suspended類型優先級的時候),nextExpirationTimeToWorkOn表明的是準備處理的優先級,大於或者等於這個優先級的fiber節點都會獲得處理;expirationTime固然表明的是root總體的優先級,會用來跟其餘root來比較,看誰應該更優先處理。
    不過總的來講應該是React爲了支持Suspend這個特性引入的複雜度,固然複雜度還不僅這裏,若是把Suspend相關的代碼去掉,總體會很清爽,Suspend這個特性是否有這麼大的價值,在後面的章節再具體分析一下。

  4. 若是不在更新過程當中,或者這個Root跟當前調度的Root不同,把這個Root也加入到調度隊列裏面,若是優先級比當前調度的Root更高,就會請求一次新的調度。

我的總結

  1. React利用Scheduler尋找一個合適的執行時機
  2. 在這個合適的時機裏面ReactFiberRoot就是它的調度單位
  3. 若是被更高優先級的任務打斷,React會很是簡單粗暴放棄掉以前完成到一半的更新,等待後面調度重頭再開始
相關文章
相關標籤/搜索