React也使用了好一段時間,最近纔有空把源碼閱讀一下(真是慚愧),由於項目用的React版本比較老,因此找的版本也就對應項目使用的版本恰好是16.8.6,不過React源碼分析的文章已經不少了,並且有不少也質量很高,因此這裏僅僅當作本身的筆記做爲記錄吧。react
在還沒接觸React的時候就已經瞭解到React16的一些新特性:協程,分片等,那個時候恰好接觸到go語言對React的協程一直有幾個疑問,會不會跟go語言的協程同樣有調度器,React的調度單位是怎樣的,Fiber是怎樣搶佔,中斷和恢復執行的?git
React的調度器其實代碼量並很少,僅僅只有700多行,而後核心功能就是如下兩點:github
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
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來兜底,這樣就萬無一失了。
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); } }
分四步來分析這個方法:
標記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這個特性是否有這麼大的價值,在後面的章節再具體分析一下。