因爲有裁人消息流出+被打C的雙重衝擊,只好儘可能在被裁以前惡補一波本身的未知領域,隨時作好準備html
本文是本身在閱讀完react fiber的主流程及部分功能模塊的一篇我的總結,有些措辭、理解上不免有誤,若各位讀到這篇文章,請不要吝嗇你的評論,請提出任何質疑、批評,能夠逐行提出任何問題vue
閱讀以前須要瞭解react和fiber的基本知識,如fiber的鏈表結構,fiber的遍歷,react的基本用法,react渲染兩個階段(reconcile和commit),fiber的反作用(effectTag)node
我的推薦的fiber閱讀方法:react
react爲實現併發模式(異步渲染)設計了一個帶優先級的任務系統,若是咱們不知道這個任務系統的運做方式,永遠也不會真正瞭解react,接下來的講解默認開啓react併發模式git
首先併發模式的功能:github
先從2提及,由於咱們要對react有一個全局的理解web
首先,要知道調度算法包含一切,每個調度任務,都須要完成reconcile和commit的流程,所以ReactFiberScheduler.js即爲react最核心的模塊,完成任務調度全靠他;任務調度須要有一個調度器,細節請移步下文中的Scheduler.js;這個調度器按優先級順序保存着多個任務,firstCallbackNode爲當前任務,從最高優先級任務開始,如圖所示: 算法
你們想象一下,調度器要實現任務的優先級調度,當高優先級任務來臨時,當前運行的任務(firstCallbackNode)須要打斷,讓位給高優先級任務,這個過程必須在macrotask中完成,爲何?首先requestIdleCallback
爲macrotask,並且這樣的打斷纔是咱們須要的,由於若是在主線程來調度,用戶的交互會被js運行卡住,你想打斷都打斷不了api
咱們通常會在哪調用setState?promise
componentWillMount
中調用setState;該生命週期的執行會運行在reconcile階段,不加入任務調度器componentDidMount
中調用setState;該生命週期的執行會運行在commit階段,react中有一個isRendering
標誌,true表示reconcile+commit正在進行,任務加入調度器須要isRendering
爲false,不加入任務調度器又因爲初始化渲染不開啓併發模式,所以調度器中只會有三種來源的任務:
unstable_scheduleCallback
以調用setState而建立的任務在1和3中,setState就真的成了異步更新了;對於1和3,react也會作一個合併的處理,將全部setState合併,如:
setTimeout(() => {
this.setState({
nums: this.state.nums + 1
})
this.setState({
nums: this.state.nums + 1
})
}, 0)
複製代碼
若state.nums初始值爲0,在非併發模式下,最終會更新到2,由於setState是同步的;而在併發模式下,nums最終仍然爲1,由於第二個setState任務沒法加入調度器;來源1和來源3都是調度任務,在react調度器中,調度任務不能同時出現兩個或以上;爲何有這個規則,咱們下文再談。源碼以下:
function scheduleCallbackWithExpirationTime(
root: FiberRoot,
expirationTime: ExpirationTime,
) {
if (callbackExpirationTime !== NoWork) {
// A callback is already scheduled. Check its expiration time (timeout).
// 低級別的任務直接 return
if (expirationTime < callbackExpirationTime) {
// Existing callback has sufficient timeout. Exit.
return;
} else {
if (callbackID !== null) {
// Existing callback has insufficient timeout. Cancel and schedule a
// new one.
// 不然會取消當前,再用新的替代
cancelDeferredCallback(callbackID);
}
}
// The request callback timer is already running. Don't start a new one. } else { startRequestCallbackTimer(); } ... callbackID = scheduleDeferredCallback(performAsyncWork, {timeout}); } 複製代碼
調度器已經簡單介紹完畢,實現細節請移步下文的Scheduler.js;咱們能夠把調度器想象成一個黑盒,當咱們每調用一次併發模式的setState時,就向調度器加入一個任務,它會自動幫咱們按優先級運行
每個併發模式調用的setState都會產生一個調度任務,而每個調度任務都會完成一次渲染過程,所以我預測在併發模式正式推出後,會有大量文章針對併發模式下的setState的優化文章,至少咱們如今能夠知道併發模式下的setState可不能濫用;搞清楚了調度器和任務框架咱們再來深刻一下調度器中的每一個任務
有了上文的總體框架,我以爲這個時候你們能夠本身去看看源碼了,我只要告訴你performAsyncWork
表明向調度器加入一個異步調度任務,而performSyncWork
表明了主線程開始運行一個同步任務,源碼閱讀就不困難了
先簡單思考一下,一個調度任務須要完成什麼工做:
requestIdleCallback
就好了,它只能幫咱們儘可能分配好時間來運行JS,詳見下文;若是你的callback運行時間太長,它是沒辦法的,由於只要JS運行就會卡交互先驗知識:
ReactDOM.render(<App/>, document.getElementById('root'))
所建立,正常來講一個項目應該只有一個fiber.expirationTime
屬性表明該fiber的優先級,數值越大,優先級越高,經過當前時間減超時時間得到,同步任務優先級默認爲最高fiber.childExpirationTime
屬性表明該fiber的子節點優先級,該屬性能夠用來判斷fiber的子節點還有沒有任務或比較優先級,更新時若沒有任務(fiber.childExpirationTime===0
)或者本次渲染的優先級大於子節點優先級,那麼沒必要再往下遍歷 當某組件(fiber)觸發了任務時,會往上遍歷,將fiber.return.expirationTime
和fiber.return.childExpirationTime
所有更新爲該組件的expirationTime,一直到root,能夠理解爲在root上收集更新先看看調度任務的總流程:
從setState開始,區分同步或異步,同步則直接運行performWork
,異步則將performWork
加入到macrotask運行(調度器);再根據isYieldy
(是否能打斷,同步則不能打斷,爲false) 來調用不一樣的performRoot
循環體;圖中綠線表明異步任務,紅框表示該過程可被打斷;任務未執行完畢的話(被打斷),這裏會重複向調度器加入任務
注意:這裏的打斷表明macrotask中該任務已運行完畢,會把js運行交還給主線程,也是用戶交互能獲得喘息的惟一機會
再看看performRoot循環體:
循環判斷是否還有任務以及!(didYield && currentRendererTime > nextFlushedExpirationTime)
,didYield
表示是否已經被調度器叫停;currentRendererTime
能夠理解爲任務運行的當前時間,經過recomputeCurrentRendererTime()
獲得,上文說過,隨着時間流逝,該值愈來愈小;nextFlushedExpirationTime
表示將要渲染的任務的時間(root.expirationTime);當兩個表達式都爲true時,循環才退出,didYield
爲true說明任務被調度器叫停,須要被打斷,currentRendererTime > nextFlushedExpirationTime
爲true代表任務未超時
這裏我認爲判斷有些重複,由於調度器已經爲咱們判斷了是否超時,超時則不會打斷,我認爲react在這裏是一個雙保險機制,具體緣由未知
進入循環,執行performWorkOnRoot()
,這個稍後再講;接下來是findHighestPriorityRoot()
,其實就是找最高優先級的root,並獲得root的expirationTime,root的expirationTime即爲將要執行的任務的時間即這裏的nextFlushedExpirationTime
;最後是算當前時間
再看看performWorkOnRoot()
:
我已合併同步異步的狀況,綠線表示異步多出來的部分;代碼很簡單,就是判斷finishedWork
是否爲空,爲空則renderRoot()
,不爲空則completeRoot()
,這裏renderRoot
即爲reconcile過程,completeRoot
即爲commit過程,接下來看reconcile過程
renderRoot
其實只作兩件事:
workLoop
循環體nextUnitOfWork
是否爲空,若不爲空則任務未完成,下次再繼續,爲空則代表reconcile完成,賦值root.finishedWork
,這時候才能commitworkLoop循環體:
看的出來workLoop
即爲循環求nextUnitOfWork
的過程,直到nextUnitOfWork
爲空或者被打斷;nextUnitOfWork
是一個全局變量,就是遍歷所在的fiber,那麼workLoop
就是不斷地遍歷,求出下一個fiber;先執行beginWork
,beginWork
作了什麼?
processUpdateQueue
(參考後文)獲得state,調用render
渲染函數以獲得JSX表示的虛擬節點,標記componentDidMount等反作用若是beginWork
的結果爲空,說明這個節點已經沒有兒子了,接下來就該輪到completeUnitOfWork
出場了,completeUnitOfWork
須要作到:
reconcile完成之後,接下來再回到performWorkOnRoot
中的commitRoot
,主要工做以下:
react調度任務細節總結完畢,我並無說太多reconcile和commit的細節,由於我認爲這部分寫多了就不叫總結了,遠不如本身讀來的清楚
將整個react項目比做一個大型礦場,用戶是老闆,調度器是包工頭,調度任務是礦工,不一樣的礦場表明着不一樣的root;一個項目只能有一個礦坑(nextUnitOfWork
),虛線底部表明礦已挖完,reconcile結束
由於一個礦坑只產出一種礦,不一樣礦工來挖同一個礦坑,有的礦工挖的是金礦,有的礦工挖的是煤礦,不容許;這裏可能也有react15中setState合併的考慮
commit階段要執行componentDidMount這種react徹底失控的反作用,以及其它生命週期,固然不能打斷,否則打斷再運行,豈不是會重複調用屢次? 在reconcile階段,react一樣避免調用任何失控的代碼,如componentWillReceiveProps,componentWillReceiveProps,用戶在這些生命週期裏面調用setState,reconcile被打斷後從新開始豈不是要調用屢次setState?
若是從目標fiber開始更新,如這裏的fiber2,那麼咱們的礦坑就能夠從fiber2開始挖,節省了時間;可是你沒有想過,root的優先級是會更新的,若是這時候fiber3擁有了更高優先級,那麼會從fiber3開始遍歷,因爲遍歷只能向下或向右,咱們會忽視fiber2的更新;因此不如把全部更新提到root,這樣惟一的壞處就是被打斷以後要從root開始遍歷,可是至少不會漏掉更新
unstable_scheduleCallback
時的timeout值很是有講究,當用戶以滾鍵盤的極快速度輸入1-9時,timeout值設得太低,中間不少數字將不會被渲染! 舉例來講,當輸入1時,以unstable_scheduleCallback
來調用setState,調度器中存在的任務是setState任務,而後setState任務又建立了一個調度任務,這個調度任務不斷地打斷重連,咱們的交互獲得喘息,輸入了2,3,4...調度器中又放入了多個setState任務,由於按得太快,這些任務在調度器中被鏈接到了一塊兒;在第一個任務打斷重連完畢後,接下來的幾個setState任務所有執行並轉成了調度任務,因爲這幾個調度任務expirationTime相等,執行的倒是不一樣的setState任務,所以調度任務被合併,只會剩下最後一個執行的調度任務; 不過,當timeout值設置得夠大時,問題將獲得解決,由於這時候加入調度器的setState任務的expirationTime會很是大,它們的執行會很是靠後,在它們建立的每個調度任務執行完以後,所以輸入框的數字將渲染得很完整,不過依然沒法擺脫4的問題react在這部分的內容不少很雜,可是我認爲對主流程而言不必講的太細,何況我也沒看太仔細,這裏更多細節只須要參考這篇文章React事件系統和源碼淺析 - 掘金
簡單來講,react實現了一套事件系統;在更新props階段,就爲全部擁有事件回調的fiber綁定好事件(react事件系統),事件綁定在document上;觸發事件時,進入事件系統,事件系統建立一個SyntheticEvent
用來代替原生的e對象;接着,以冒泡/捕獲的順序收集全部fiber和其中的事件回調;再按冒泡/捕獲順序觸發綁定在fiber上的回調函數
這裏須要注意幾點:
interactiveUpdates()
裏的判斷讓人費解,找不到其場景if (
!isBatchingUpdates &&
!isRendering &&
lowestPriorityPendingInteractiveExpirationTime !== NoWork
) {
// Synchronously flush pending interactive updates.
performWork(lowestPriorityPendingInteractiveExpirationTime, false);
lowestPriorityPendingInteractiveExpirationTime = NoWork;
}
複製代碼
關鍵詞:requestAnimationFrame、frameDeadline、activeFrameTime、timeout、unstable_scheduleCallback、unstable_cancelCallback、unstable_shouldYield
核心模塊。react fiber的任務調度全靠它,我認爲搞懂這個模塊才能搞懂react schedule的過程,unstable_scheduleCallback
、unstable_cancelCallback
、unstable_shouldYield
,三個api可以分別實現將任務加入任務列表,將任務從任務列表中刪除,以及判斷任務是否應該被打斷
主要實現方法是運用requestAnimationFrame + MessageChannel + 雙向鏈表的插入排序,最後暴露出unstable_scheduleCallback
和unstable_shouldYield
兩個api。
對第一位的理解須要看一下這篇文章,簡單來講是屏幕顯示是顯示器在不斷地刷新圖像,如60Hz的屏幕,每16ms刷新一次,而1幀表明的是一個靜止的畫面,若一個dom元素從左到右移動,而咱們須要這個dom每一幀向右移動1px,60Hz的屏幕,咱們須要在16ms之內,完成向右移動的js運行和dom繪製,這樣在第二幀(17ms時)開始的時候,dom已經右移了1px,而且被屏幕給刷了出來,咱們的眼睛纔會感受到動畫的連續性,也就是常說的不掉幀。requestAnimationFrame
則給了咱們十分精確且可靠的服務。
requestIdleCallback
的功能是在每一幀的空閒時間(完成dom繪製、動畫等以後)來運行js,若這一幀的空閒時間不足,則分配到下一幀執行,再不足,分配到下下幀完成,直到超過規定的timeout時間,則直接運行js。requestAnimationFrame
能儘可能保證回調函數在一幀內運行一次且dom繪製一次,這樣也保證了動畫等效果的流暢度,然而卻沒有超時運行機制,react polyfill的主要是超時功能。
requestAnimationFrame
一般的用法:
function callback(currentTime) {
// 動畫操做
...
window.requestAnimationFrame(callback)
}
window.requestAnimationFrame(callback)
複製代碼
其表明的是每一幀都儘可能運行一次callback,並完成動畫繪製,若運行不完,也沒辦法,就掉幀。
Scheduler.js使用animationTick
做爲requestAnimationFrame
的callback,用以計算frameDeadline
和調用傳入的回調函數,在react中即爲調度函數;frameDeadline
表示的是運行到當前幀的幀過時時間,計算方法是當前時間 + activeFrameTime
,activeFrameTime
表示的是一幀的時間,默認爲33ms,可是會根據設備動態調整,好比在刷新頻率更高的設備上,連續運行兩幀的當前時間比運行到該幀的過時時間frameDeadline
都小,說明咱們一幀中的js任務耗時也小,一幀時間充足且requestAnimationFrame
調用比預設的33ms頻繁,那麼activeFrameTime
會下降以達到最佳性能
有了frameDeadline
與用戶自定義的過時時間timeoutTime
,那麼咱們很容易獲得polyfill requestIdleCallback的原理:用戶定義的callback在這一幀有空就去運行,超過幀過時時間frameDeadline
就到下一幀去運行,你能夠超過幀過時時間,可是你不能超過用戶定義的timeoutTime
,一旦超過,我啥也無論,直接運行callback。
Scheduler.js將每一次unstable_scheduleCallback
的調用根據用戶定義的timeout來爲任務分配優先級,timeout越小,優先級越高。具體實現爲:用雙向鏈表結構來表示任務列表,且按優先級從高到低的順序進行排列,當某個任務插入時,從頭結點開始循環遍歷,若遇到某個任務結點node的expirationTime > 插入任務的expirationTime,說明插入任務比node優先級高,則退出循環,並在node前插入,expirationTime = 當前時間 + timeout;這樣就實現了按優先級排序的任務插入功能,animationTick
會循環調用這些任務鏈表。
function unstable_shouldYield() {
return (
!currentDidTimeout &&
((firstCallbackNode !== null &&
firstCallbackNode.expirationTime < currentExpirationTime) ||
shouldYieldToHost())
);
}
shouldYieldToHost = function() {
return frameDeadline <= getCurrentTime();
};
複製代碼
unstable_shouldYield
被用來判斷在任務列表中是否有更高級的任務,在react中用來判斷是否能打斷當前任務,是schedule中的一個核心api。
首先判斷currentDidTimeout
,currentDidTimeout
爲false說明任務沒有過時,你們要知道過時任務擁有最高優先級,那麼即便有更高級的任務依然沒法打斷,直接return false; 再判斷firstCallbackNode.expirationTime < currentExpirationTime
,這裏其實是照顧一種特殊的狀況,那就是一個最高優先級的任務插入以後,低優先級的任務還在運行中,這種狀況是仍然須要打斷的;這裏firstCallbackNode
實際上是那個插入的高優先級任務,而currentExpirationTime
實際上是上一個任務的expirationTime,只是還沒結算
最後是一個shouldYieldToHost()
,很簡單,就是看任務在幀內是否過時,注意到這邊任務幀內過時的話是return true,表明直接就能被打斷;
關鍵詞:enqueueUpdate、processUpdateQueue
react15中,全部經交互事件觸發的setState更新都會被收集到dirtyComponents,收集好了再批量更新;react16因爲加入了優先級策略,在調度時連setState操做都被賦予不一樣的優先級,在同一組件針對帶優先級的調度任務及setState操做,是該模塊的核心功能
首先貼兩個數據結構(已刪去部分不關注的屬性):
export type Update<State> = {
expirationTime: ExpirationTime,
payload: any,
callback: (() => mixed) | null,
next: Update<State> | null,
nextEffect: Update<State> | null,
};
export type UpdateQueue<State> = {
baseState: State,
// 頭節點
firstUpdate: Update<State> | null,
// 尾節點
lastUpdate: Update<State> | null,
// callback 處理
firstEffect: Update<State> | null,
lastEffect: Update<State> | null,
};
複製代碼
fiber上有個updateQueue
屬性,就是來自上述數據結構。每次調用setState的時候,會新建一個updateQueue
,queue中存儲了baseState
,用於記錄state,該屬性服務於優先級調度,後面會說;另外記錄頭節點、尾節點及用於callback的effect頭尾指針;還有以鏈表形式鏈接的update,如圖所示:
每當調用一次setState,會調用enqueueUpdate
,就會在鏈表以後插入一個update,這個插入是無序的,然而不一樣的update是帶優先級的,用一個屬性expirationTime來表示,payload即爲調用setState的第一個參數。
當調度任務依次執行時,會調用processUpdateQueue
計算最終的state,咱們不要忘了調度任務是帶有優先級的任務,執行的時候有前後順序,對應的是processUpdateQueue
的前後執行順序;而update也是優先級任務的一部分,當咱們按鏈表順序從頭至尾執行時,須要優先執行高優先級的update,跳太低優先級的update;react的註釋爲咱們闡明瞭這一過程:
假設有一updateQueue爲A1 - B2 - C1 - D2;
A一、B2等表明一個update,其中字母表明state,數字大小表明優先級,1爲高優先級;
調度任務按高低優先級依次執行,第一次調度是高優先級任務,從頭結點firstUpdate開始處理,processUpdateQueue會跳太低優先級的update;
則執行的update爲A1 - C1,本次調度獲得的最終state爲AC,baseState爲A,queue的firstUpdate指針指向B2,以供下次調度使用;
第二次調度是低優先級任務,此時firstUpdate指向B2,則從B2開始,執行的update爲 B2 - C1 - D2,最終state將與baseState:A合併,獲得ABCD
以上即爲processUpdateQueue
的處理過程,咱們須要注意幾點:
processUpdateQueue
從頭結點firstUpdate
開始遍歷update,並對state進行合併 對於低優先級的update,遍歷時會跳過baseState
會定格在被跳過的update以前的resultStatebaseState
主要做用在於記錄好被跳過的update以前的state,以便在下一次更加低優先級的調度任務時合併statefirstUpdate
和lastUpdate
指向null,updateQueue
完成使命再看一個例子:
A1-B1-C2-D3-E2-F1 第一次調度:baseState:AB,resultState:ABF,firstUpdate:C2 第二次調度:baseState:ABC,resultState:ABCEF,firstUpdate:D3 第三次調度:baseState:ABC,resultState:ABCDEF,firstUpdate:null