系列文章
React Fiber源碼分析 第一篇
React Fiber源碼分析 第二篇(同步模式)
React Fiber源碼分析 第三篇(異步狀態)
React Fiber源碼分析 第四篇(歸納總結)
前言
React Fiber是React在V16版本中的大更新,利用了閒餘時間看了一些源碼,做個小記錄~
什麼是Fiber
從開發者角度來看
實際上這次更新對於我們來說影響並不大,只是幾個生命週期改變了(React在版本中的更新簡直做到了像一門語言一樣,完美的兼容老版本,底層算法的大重構對於開發者來說完全透明),新引入的兩個生命週期函數 getDerivedStateFromProps,getSnapshotBeforeUpdate 以及在未來 v17.0 版本中即將被移除的三個生命週期函數componentWillMount,componentWillReeiveProps,componentWillUpdate,目前版本並不會影響原生命週期的使用,但不能和新的生命週期一起使用,也會被標記爲不安全,下圖爲目前React的流程圖
其他的幾乎沒有任何影響,我們還是照常的寫着原來的代碼,然後我們就感覺到網頁性能更高了一些。
爲什麼網頁性能會變高
要回答這個問題,需要回頭看javascript是單線程的知識點。
單線程一次只能做一件事, 在原來的React中, 如果一次更新的時間比較長,那麼用戶就會感覺到卡頓,也就是丟幀了。
打個比方, 假如我現在要更新1000個組件(往大了說),每個組件平均花時間1ms,那麼在1s內,瀏覽器的整個線程都被阻塞了,這時候用戶在input上的任何操作都不會有反應,等到更新完畢,界面上突的一下就顯示了原來用戶的輸入,這個體驗是非常差的。這裏借用官方一張圖, Fiber之前的版本就是這樣,調用棧非常深
那麼Fiber,現在是怎麼做呢?
Fiber實際上是把一次更新拆成一個個的單元任務,每次做完一個單元任務後,就詢問是否有更高的優先級任務,有就去執行,回頭再來幹這件事,如圖
那麼就明白了,Fiber是一個任務調和器!, 同樣,我們根據這個來分析Fiber具體做了什麼
Fiber具體做了什麼
首先,要做到這樣的效果,那麼就需要有以下的功能:
- 任務可分片 (拆分任務)
- 任務可中斷 (執行另一個任務後, 可以回頭繼續執行未完成的任務)
- 具備優先級 (哪個任務先執行)
任務可分片
在React中,無論是state還是props的更新, 最後都操作在JSX的標籤上
利用這種天然友好的表達,直接把每一個標籤當成一個任務分片如:div、p1、p2、span都是一個任務分片
<div>
<p>p1</p>
<p>
<span>p2</span>
</p>
</div>
當然, 還要從標籤轉換成VDOM,再轉成Fiber,纔是一個真正的任務片,如圖:
fiber的數據結構
任務可中斷
Fiber之前React是通過棧調度器進行遞歸更新,畢竟標籤化是天然嵌套的,對遞歸友好,但是遞歸不好break和continue
從大遞歸到大循環
Fiber則是以鏈表的形式來進行逐步更新(深度優先遍歷算法),鏈表對break和continue友好Fiber節點擁有return, child, sibling三個屬性,分別對應父節點, 第一個孩子, 它右邊的兄弟,
(圖來自網絡,侵刪)
如何回到中斷
任務中斷,執行高優先級任務後如何回來被中斷的任務
React內部維護一個任務鏈表,每次某個任務結束後都會刪除已完成的任務並繼續執行其他可執行的任務,每個任務都有一個finishedWork屬性,如果該屬性不爲null,則說明更新完畢,只差commit render階段
回到中斷任務後,如何從中斷的任務片開始
這個主要依賴於fiber中的兩個屬性expirationTime和childExpirationTime,當某個fiber被執行完畢後,會把expirationTime設爲NoWork,即被打斷後可以通過該屬性判斷任務碎片是否
需要執行
this.expirationTime = NoWork // 任務優先級
this.childExpirationTime = NoWork // 子任務片的優先級
任務中斷再執行的流程
- 通過深度遍歷搜索算法對每一個fiber即任務碎片進行更新
- 每一個任務碎片完成後會將expirationTime設爲NoWork
- 假設此時有更高優先級的任務,則執行更高優先級任務
- 任務執行完成後,會從任務列表中剔除,並繼續執行其他未完成且可以執行的任務。
- 回到被打斷任務,可以通過任務的finishWork屬性判斷是否需要執行更新
- 根據任務碎片的expirationTime判斷是否需要執行更新
中斷更新階段其他屬性介紹
Alternater
每次更新都不會對fiber直接操作,而是克隆一個作爲alternater屬性
updateQueue
更新隊列, 存放更新的信息
Effect
收集更新信息,生成真實DOM
具備優先級
每個Root任務更新任務fiber都具有expirationTime屬性,該屬性即爲優先級expirationTime越小,優先級越高,同步模式下該值爲0, 每個層級的任務都是以鏈表的形式存在
爲什麼採用時間作爲優先級屬性
這時候就是requestIdleCallback這個API的騷操作了, 這個API是幹嘛的呢?
window.requestIdleCallback()會在瀏覽器空閒時期依次調用函數, 這就可以讓開發者在主事件循環中執行後臺或低優先級的任務,而且不會對像動畫和用戶交互這樣延遲觸發而且關鍵的事件產生影響。函數一般會按先進先調用的順序執行,除非函數在瀏覽器調用它之前就到了它的超時時間。
也就是說React實際上利用這個API在瀏覽器空閒期執行任務, 而這個API的回調有個參數deadline , 當你超時的時候,無論是不是在空閒期都會執行該任務, 這也就解釋了爲什麼React採用時間來做優先級
不過實際上, React並沒有在版本中使用了這個API,而是通過requestAnimationFrame來hack,強行設置每一幀的到期時間爲requestAnimationFrame回調函數的參數加上33ms
var animationTick = function (rafTime) {
isAnimationFrameScheduled = false;
...
...
// 每幀到期時間爲33ms
frameDeadline = rafTime + 33
if (!isIdleScheduled) {
isIdleScheduled = true;
window.postMessage(messageKey, '*');
}
};
當然了, 分優先級是有一個無法避免的問題, 那就是當有無數的優先級更高的任務插進來, 就會形成飢餓現象,原有的任務會一直得不到機會執行
總結
React Fiber實際上就是一個任務調和器,它做到了將每一次更新切分成任務分片,從而擁有了可中斷且有優先級的進行其他任務的功能。
在分析的過程中,發現了React的源碼中使用了很多鏈式結構, 回調鏈,任務鏈等,這個主要是爲了增刪時性能比較高