React知識點總結(Fiber&Diff)
文章目錄
- React知識點總結(Fiber&Diff)
- 一、Fiber模型是什麼
-
- 1.代數效應
- 2.代數效應在React中的展現
- 3.React Fiber
- 4.Fiber的起源
- Fiber的結構
-
- (一)作為架構
- (二)作為靜态的資料結構
- (三)作為動态的更新單元
- 二、Fiber樹是如何建構/更新的
-
- 1.“遞”與“歸”
-
- “遞”
- “歸”
- 2.beginWork方法都做了什麼?
- 3.completeWork做了什麼?
- 三、Diff算法
-
- 單節點
- 多節點
-
- 第一輪
- 第二輪
-
- 1.newChildren與oldFiber同時周遊完
- 2.newChildren沒周遊完,oldFiber周遊完
- 3.newChildren周遊完,oldFiber沒周遊完
- 4.newChildren與oldFiber都沒周遊完
- 四、狀态更新
-
- 狀态更新的流程圖
- Update
-
- Update的分類
- Update與fiber的關系(class元件)
- 五、優先級
-
- 如何保證Update對象不丢失
- 如何保證狀态依賴的連續性
- 六、commit階段中的useEffect執行時機
一、Fiber模型是什麼
1.代數效應
代數效應是函數式程式設計中的一個概念,是指将函數的副作用抽離出函數,隻關心函數的功能。(可以簡單了解為在不同使用場景像替代數字一樣簡單。。。在使用時使用規則不會變化,來限制使用)
2.代數效應在React中的展現
典型的例子就是
React
中的
hooks
,我們不需要關心内部實作,且在函數元件中使用時用法都相同,不會有什麼副作用,隻需關注業務邏輯即可。
function App() {
const [num, updateNum] = useState(0);
return (
<button onClick={() => updateNum(num => num + 1)}>{num}</button>
)
}
3.React Fiber
React Fiber
是
React
一套狀态更新機制,支援中斷和恢複以及不同優先級,更新單元為
React Element
對應的
Fiber
對象。
4.Fiber的起源
在React15及以前,
Reconciler
采用遞歸的方式建立虛拟DOM,遞歸過程是不能中斷的。如果元件樹的層級很深,遞歸會占用線程很多時間,造成卡頓。
為了解決這個問題,React16将
重構為
遞歸的無法中斷的更新
,由于曾經用于遞歸的虛拟DOM資料結構已經無法滿足需要。于是,全新的Fiber架構應運而生。
異步的可中斷更新
Fiber的結構
(一)作為架構
每個
Fiber
對象是靠什麼來構成
Fiber
樹的呢,靠一下三個屬性:
// 指向父級Fiber節點
this.return = null;
// 指向子Fiber節點
this.child = null;
// 指向右邊第一個兄弟Fiber節點
this.sibling = null;
如:
(二)作為靜态的資料結構
每個Fiber對象儲存了
DOM
對象/元件相關的資訊
// Fiber對應元件的類型 Function/Class/Host...
this.tag = tag;
// key屬性
this.key = key;
// 大部分情況同type,某些情況不同,比如FunctionComponent使用React.memo包裹
this.elementType = null;
// 對于 FunctionComponent,指函數本身,對于ClassComponent,指class,對于HostComponent,指DOM節點tagName
this.type = null;
// Fiber對應的真實DOM節點
this.stateNode = null;
(三)作為動态的更新單元
每個
Fiber
對象儲存了此次更新相關的資訊:
// 儲存本次更新造成的狀态改變相關資訊
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// 儲存本次更新會造成的DOM操作
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 排程優先級相關
this.lanes = NoLanes;
this.childLanes = NoLanes;
二、Fiber樹是如何建構/更新的
這一過程發生在
render
階段
1.“遞”與“歸”
通過周遊的方式實作可以中斷的遞歸:
“遞”
從
rootFiber
開始深度優先周遊,每一個
Fiber
對象都調用
beginWork
方法,這個方法會為傳入的
Fiber
對象建立子
Fiber
對象,并将兩者連接配接起來。當周遊到葉子節點(沒有子元件了)就進入“歸”階段
“歸”
在此階段會調用
completeWork
方法,來處理
Fiber
對象。若目前對象還有兄弟節點,則處理完目前節點後會進入兄弟節點的“遞”階段。當執行到
rootFiber
時
render
階段就結束了。
2.beginWork方法都做了什麼?
3.completeWork做了什麼?
三、Diff算法
一般的
Diff
算法,時間複雜度在
O(n3)
,
React
為了提升效率,提出了三個限制條件:
- 按層來比較元素,如果跨層則不會進行移動操作,而會重新生成元素。
- 不同的類型的節點會産生不同的樹如果元素由
變為div
,p
會銷毀React
及其子孫節點,并建立div
及其子孫節點。p
- 使用
屬性來定位元素key
前後位置的變化Diff
可以将
Diff
算法分為兩類:單節點和多節點(指的是
workInProgressFiber
,即新的
fiber
節點)。
單節點
React通過先判斷
key
是否相同,如果
key
相同則判斷
type
是否相同,隻有都相同時一個
DOM
節點才能複用。
注意
!
1.當
!==
child
且
null
相同且
key
不同時執行
type
将
deleteRemainingChildren
及其兄弟
child
fiber
都标記删除。
2.當
!==
child
且
null
不同時僅将
key
标記删除。
child
如:
// 目前頁面顯示的
ul > li * 3
// 這次需要更新的
ul > p
p
的
key
和
li
的
key
不同,說明目前以及後面的兄弟元素都不可能有相同的
key
,則沒有必要繼續周遊,不可複用之前的節點,需生成新的節點。
多節點
分兩輪周遊
第一輪
-
,周遊let i = 0
,将newChildren
與newChildren[i]
比較,判斷oldFiber
節點是否可複用。DOM
- 如果可複用,
,繼續比較i++
與newChildren[i]
,可以複用則繼續周遊。oldFiber.sibling
- 如果不可複用,分兩種情況:
不同導緻不可複用,立即跳出整個周遊,第一輪周遊結束。key
相同key
不同導緻不可複用,會将type
标記為oldFiber
,并繼續周遊。DELETION
- 如果
newChildren
周遊完(即i === newChildren.length -
1)或者oldFiber周遊完(即oldFiber.sibling === null),跳出周遊,第一輪周遊結束。
第二輪
1.newChildren與oldFiber同時周遊完
那就是最理想的情況:隻需在第一輪周遊進行元件更新 。此時
Diff
結束。
2.newChildren沒周遊完,oldFiber周遊完
已有的
DOM
節點都複用了,這時還有新加入的節點,意味着本次更新有新節點插入,我們隻需要周遊剩下的
newChildren
為生成的
workInProgress fiber
依次标記
Placement
。
3.newChildren周遊完,oldFiber沒周遊完
意味着本次更新比之前的節點數量少,有節點被删除了。是以需要周遊剩下的
oldFiber
,依次标記
Deletion
。
4.newChildren與oldFiber都沒周遊完
這意味着有節點在這次更新中改變了位置。
用一個簡單的Demo來看一下整個判斷的過程
// 之前
abcd
// 之後
acdb
===第一輪周遊開始===
a(之後)vs a(之前)
key不變,可複用
此時 a 對應的oldFiber(之前的a)在之前的數組(abcd)中索引為0
是以 lastPlacedIndex = 0;
繼續第一輪周遊...
c(之後)vs b(之前)
key改變,不能複用,跳出第一輪周遊
此時 lastPlacedIndex === 0;
===第一輪周遊結束===
===第二輪周遊開始===
newChildren === cdb,沒用完,不需要執行删除舊節點
oldFiber === bcd,沒用完,不需要執行插入新節點
将剩餘oldFiber(bcd)儲存為map
// 目前oldFiber:bcd
// 目前newChildren:cdb
繼續周遊剩餘newChildren
key === c 在 oldFiber中存在
const oldIndex = c(之前).index;
此時 oldIndex === 2; // 之前節點為 abcd,是以c.index === 2
比較 oldIndex 與 lastPlacedIndex;
如果 oldIndex >= lastPlacedIndex 代表該可複用節點不需要移動
并将 lastPlacedIndex = oldIndex;
如果 oldIndex < lastplacedIndex 該可複用節點之前插入的位置索引小于這次更新需要插入的位置索引,代表該節點需要向右移動
在例子中,oldIndex 2 > lastPlacedIndex 0,
則 lastPlacedIndex = 2;
c節點位置不變
繼續周遊剩餘newChildren
// 目前oldFiber:bd
// 目前newChildren:db
key === d 在 oldFiber中存在
const oldIndex = d(之前).index;
oldIndex 3 > lastPlacedIndex 2 // 之前節點為 abcd,是以d.index === 3
則 lastPlacedIndex = 3;
d節點位置不變
繼續周遊剩餘newChildren
// 目前oldFiber:b
// 目前newChildren:b
key === b 在 oldFiber中存在
const oldIndex = b(之前).index;
oldIndex 1 < lastPlacedIndex 3 // 之前節點為 abcd,是以b.index === 1
則 b節點需要向右移動
===第二輪周遊結束===
最終acd 3個節點都沒有移動,b節點被标記為移動
四、狀态更新
狀态更新的流程圖
觸發狀态更新(根據場景調用不同方法)
|
|
v
建立Update對象(給觸發狀态更新的fiber對象建立)
|
|
v
從fiber到root(`markUpdateLaneFromFiberToRoot`由于render階段是從rootFiber向下執行的,為了得到rootFiber,從觸發狀态更新的fiber得到rootFiber,同時包含給不同的fiber标記優先級)
|
|
v
排程更新(`ensureRootIsScheduled`)
|
|
v
render階段(`performSyncWorkOnRoot` 或 `performConcurrentWorkOnRoot` 同步更新還是異步更新)
|
|
v
commit階段(`commitRoot`)
Update
Update的分類
- ReactDOM.render —— HostRoot
- this.setState —— ClassComponent
- this.forceUpdate —— ClassComponent
- useState —— FunctionComponent
- useReducer —— FunctionComponent
Update與fiber的關系(class元件)
fiber
節點存在着
updateQueue
,
updateQueue
為儲存更新對象的連結清單。
fiber.updateQueue.baseUpdate: u1 --> u2 --> u3 --> u4
接下來周遊updateQueue.baseUpdate連結清單,以
fiber.updateQueue.baseState
為初始
state
,依次與周遊到的每個
Update
計算并産生新的
state
。在周遊時如果有優先級低的
Update
會被跳過。
state
的變化在
render
階段産生與上次更新不同的
JSX
對象,通過
Diff
算法産生
effectTag
,在
commit
階段渲染在頁面上。
五、優先級
通過一張圖來了解一下
React
的優先級:
在這個例子中,
u1
的優先級較
u2
的優先級低,首先
u1
進入
render
階段,當産生
u2
時,中斷
u1
,
u2
進入
render
階段。此時,該
Update
對象儲存的連結清單是:
u1 -- u2
由于
u1
的優先級較低,
u1
會被跳過,
u2
會進入
render-commit
階段。在
commit
階段再排程一次更新,這次是基于
baseState
中
firstBaseUpdate
儲存的
u1
,開啟一次新的
render
階段。
如何保證Update對象不丢失
在
render
階段,
shared.pending
的環被剪開并連接配接在
updateQueue.lastBaseUpdate
後面。實際上
shared.pending
會被同時連接配接在
workInProgress updateQueue.lastBaseUpdate
與
current updateQueue.lastBaseUpdate
後面。
當開啟一個新的
render
時,會基于
current updateQueue
克隆出
workInProgress updateQueue
,是以不會丢失。
如何保證狀态依賴的連續性
當某個
Update
優先級低于目前優先級時,目前
Update
對象和之後的
Update
對象會被跳過,儲存到
baseUpdate
中作為下次
Update
更新的
Update
。且目前的
baseState
為更新後的
state
,不是被跳過時的
state
。
六、commit階段中的useEffect執行時機
-
階段在before mutation
中排程scheduleCallback
flushPassiveEffects
-
階段之後将layout
指派給effectList
rootWithPendingPassiveEffects
-
觸發scheduleCallback
,flushPassiveEffects
内部周遊flushPassiveEffects
rootWithPendingPassiveEffects
參考文章:
React技術揭秘