點選進入React原始碼除錯倉庫。
上一篇扒一扒React計算狀態的原理 之後,我們來分析一下Diff的過程。
fiber上的updateQueue經過React的一番計算之後,這個fiber已經有了新的狀態,也就是state,對於類元件來說,state是在render函式裡被使用的,既然已經得到了新的state,那麼當務之急是執行一次render,得到持有新state的ReactElement。
假設render一次之後得到了大量的ReactElement,而這些ReactElement之中若只有少量需要更新的節點,那麼顯然不能全部去更新它們,此時就需要有一個diff過程來決定哪些節點是真正需要更新的。
原始碼結構
我們以類元件為例,state的計算髮生在類元件對應的fiber節點beginWork中的updateClassInstance
函式中,在狀態計算完畢之後,緊跟著就是去調finishClassComponent
執行diff、打上effectTag(即新版本的flag)。
打上effectTag可以標識這個fiber發生了怎樣的變化,例如:新增(Placement)、更新(Update)、刪除(Deletion),這些被打上flag的fiber會在complete階段被收集起來,形成一個effectList連結串列,只包含這些需要操作的fiber,最後在commit階段被更新掉。
function updateClassComponent(
current: Fiber | null, workInProgress: Fiber, Component: any, nextProps: any, renderLanes: Lanes,) {
...
// 計算狀態
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderLanes,
);
...
// 執行render,進入diff,為fiber打上effectTag
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderLanes,
);
return nextUnitOfWork;
}
在finishClassComponent
函式中,呼叫reconcileChildFibers
去做diff,而reconcileChildFibers
實際上就是ChildReconciler
,這是diff的核心函式,
該函式針對元件render生成的新節點的型別,呼叫不同的函式進行處理。
function ChildReconciler(shouldTrackSideEffects) {
...
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
// 單節點diff
}
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
): Fiber | null {
// 多節點diff
}
...
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any, lanes: Lanes,
): Fiber | null {
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
// 處理單節點
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_PORTAL_TYPE:
...
case REACT_LAZY_TYPE:
...
}
}
if (typeof newChild === 'string' || typeof newChild === 'number') {
// 處理文字節點
}
if (isArray(newChild)) {
// 處理多節點
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
...
}
return reconcileChildFibers;
}
Diff的主體
關於Diff的參與者,在reconcileChildren函式的入參中可以看出
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
- workInProgress:作為父節點傳入,新生成的第一個fiber的return會被指向它。
- current.child:舊fiber節點,diff生成新fiber節點時會用新生成的ReactElement和它作比較。
- nextChildren:新生成的ReactElement,會以它為標準生成新的fiber節點。
- renderLanes:本次的渲染優先順序,最終會被掛載到新fiber的lanes屬性上。
可以看出,diff的兩個主體是:oldFiber(current.child)和newChildren(nextChildren,新的ReactElement),它們是兩個不一樣的資料結構。
比如現在有元件<Example/>,它計算完新的狀態之後,要基於這兩個東西去做diff,分別是現有fiber樹中(current樹)<Example/>對應fiber的所有子fiber節點和<Example/>的render函式的執行結果,即那些ReactElements。
<Example/>對應fiber的所有子fiber節點:oldFiber
current樹中
<Example/> fiber | | A --sibling---> B --sibling---> C
<Example/>的render函式的執行結果,newChildren
current fiber 對應的元件render的結果
[
{$$typeof: Symbol(react.element), type: "div", key: "A" },
{$$typeof: Symbol(react.element), type: "div", key: "B" },
{$$typeof: Symbol(react.element), type: "div", key: "B" },
]
Diff的基本原則
對於新舊兩種結構來說,場景有節點自身更新、節點增刪、節點移動三種情況。面對複雜的情況,即使最前沿的演算法,複雜度也極高。面對這種情況,React以如下策略應對:
- 即使兩個元素的子樹完全一樣,但前後的父級元素不同,依照規則div元素及其子樹會完全銷燬,並重建一個p元素及其子樹,不會嘗試複用子樹。
舊
<div>
<span>a</span>
<span>b</span>
</div>
新
<p>
<span>a</span>
<span>b</span>
</p>
- 使用tag(標籤名)和 key識別節點,區分出前後的節點是否變化,以達到儘量複用無變化的節點。
舊
<p key="a">aa</p>
<h1 key="b">bb</h1>
新
<h1 key="b">bb</h1>
<p key="a">aa</p>
因為tag 和 key的存在,所以React可以知道這兩個節點只是位置發生了變化。
場景
上面說到diff演算法應對三種場景:節點更新、節點增刪、節點移動
,但一個fiber的子元素有可能是單節點,也有可能是多節點。所以依據這兩類節點可以再細分為:
- 單節點更新、單節點增刪。
- 多節點更新、多節點增刪、多節點移動。
什麼是節點的更新呢?對於DOM節點來說,在前後的節點型別(tag)和key都相同的情況下,節點的屬性發生了變化,是節點更新。若前後的節點tag或者key不相同,Diff演算法會認為新節點和舊節點毫無關係。
以下例子中,key為b的新節點的className發生了變化,是節點更新。
舊
<div className={'a'} key={'a'}>aa</div>
<div className={'b'} key={'b'}>bb</div>
新
<div className={'a'} key={'a'}>aa</div>
<div className={'bcd'} key={'b'}>bb</div>
以下例子中,新節點的className雖然有變化,但key也變化了,不屬於節點更新
舊
<div className={'a'} key={'a'}>aa</div>
<div className={'b'} key={'b'}>bb</div>
新
<div className={'a'} key={'a'}>aa</div>
<div className={'bcd'} key={'bbb'}>bb</div>
以下例子中,新節點的className雖然有變化,但tag也變化了,不屬於節點更新
舊
<div className={'a'} key={'a'}>aa</div>
<div className={'b'} key={'b'}>bb</div>
新
<div className={'a'} key={'a'}>aa</div>
<p className={'bcd'} key={'b'}>bb</p>
下面來分開敘述一下單節點和多節點它們各自的更新策略。
單節點
若元件產出的元素是如下的型別:
<div key="a">aa</div>
那麼它最終產出的ReactElement為下面這樣(省略了一些與diff相關度不大的屬性)
{
$$typeof: Symbol(react.element), type: "div", key: "a"
...
}
單節點指newChildren為單一節點,但是oldFiber的數量不一定,所以實際有如下三種場景:
為了降低理解成本,我們用簡化的節點模型來說明問題,字母代表key。
- 單箇舊節點
舊: A
新: A
- 多箇舊節點
舊: A - B - C
新: B
- 沒有舊節點
舊: --
新: A
對於單節點的diff,其實就只有更新操作,不會涉及位移和位置的變化,單節點的更新會呼叫reconcileSingleElement
函式處理。該函式中對以上三種場景都做了覆蓋。但實際上面的情況對於React來說只是兩種,oldFiber鏈是否為空。因此,在實現上也只處理了這兩種情況。
oldFiber鏈不為空
遍歷它們,找到key相同的節點,然後刪除剩下的oldFiber節點,再用匹配的oldFiber,newChildren中新節點的props來生成新的fiber節點。
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
if (child.key === key) {
switch (child.tag) {
case Fragment:
...
case Block:
...
default: {
if (child.elementType === element.type) {
// 先刪除剩下的oldFiber節點
deleteRemainingChildren(returnFiber, child.sibling);
// 基於oldFiber節點和新節點的props新建新的fiber節點
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber; return existing;
}
break;
}
}
deleteRemainingChildren(returnFiber, child);
break;
} else {
// 沒匹配到說明新的fiber節點無法從oldFiber節點新建
// 刪除掉所有oldFiber節點
deleteChild(returnFiber, child);
}
child = child.sibling;
}
...
}
oldFiber鏈為空
對於沒有oldFiber節點的情況,只能新建newFiber節點。邏輯不復雜。
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
// oldFiber鏈非空的處理
...
} if (element.type === REACT_FRAGMENT_TYPE) {
// 處理Fragment型別的節點
...
} else {
// 用產生的ReactElement新建一個fiber節點
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
單節點的更新就是這樣的處理,真正比較複雜的情況是多節點的diff。因為它涉及到節點的增刪和位移。
多節點
若元件最終產出的DOM元素是如下這樣:
<div key="a">aa</div>
<div key="b">bb</div>
<div key="c">cc</div>
<div key="d">dd</div>
那麼最終的newChildren為下面這樣(省略了一些與diff相關度不大的屬性)
[
{$$typeof: Symbol(react.element), type: "div", key: "a" },
{$$typeof: Symbol(react.element), type: "div", key: "b" },
{$$typeof: Symbol(react.element), type: "div", key: "c" },
{$$typeof: Symbol(react.element), type: "div", key: "d" }
]
多節點的變化有以下四種可能性。
- 節點更新
舊: A - B - C
新: `A - B - C`
- 新增節點
舊: A - B - C
新: A - B - C - `D - E`
- 刪除節點
舊: A - B - C - `D - E`
新: A - B - C
- 節點移動
舊: A - B - C - D - E
新: A - B - `D - C - E`
多節點的情況一定是屬於這四種情況的任意組合,這種情況會呼叫reconcileChildrenArray
進行diff。按照以上四種情況,它會以newChildren為主體進行最多三輪遍歷,但這三輪遍歷並不是相互獨立的,事實上只有第一輪是從頭開始的,之後的每一輪都是上輪結束的斷點繼續。實際上在平時的實踐中,節點自身的更新是最多的,所以Diff演算法會優先處理更新的節點。因此四輪遍歷又可以按照場景分為兩部分:
第一輪是針對節點自身屬性更新,剩下的兩輪依次處理節點的新增、移動,而重點又在移動節點的處理上,所以本文會著重講解節點更新和節點移動的處理,對刪除和新增簡單帶過。
節點更新
第一輪從頭開始遍歷newChildren,會逐個與oldFiber鏈中的節點進行比較,判斷節點的key或者tag是否有變化。
- 沒變則從oldFiber節點clone一個props被更新的fiber節點,新的props來自newChildren中的新節點,這樣就實現了節點更新。
- 有變化說明不滿足複用條件,立即中斷遍歷進入下邊的遍歷。Diff演算法的複雜度也因為這個操作大幅降低。
let newIdx = 0;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
...
// 更新節點,對於DOM節點來說,updateSlot內部會判斷
// key 和 tag。任意一個不同,則返回null
const newFiber = updateSlot( returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
// newFiber為null則說明當前的節點不是更新的場景,中止這一輪迴圈
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
...
}
我們來看一個例子,假設新舊的節點如下:
舊: A - B - C - D
- E
新: A - B - D - C
在本輪遍歷中,會遍歷A - B - D - C
。A和B都是key沒變的節點,可以直接複用,但當遍歷到D時,發現key變化了,跳出當前遍歷。例子中A 和 B是自身發生更新的節點,後面的D 和 C我們看到它的位置相對於oldFiber鏈發生了變化,會往下走到處理移動節點的迴圈中。
關於移動節點的參照物
為了方便說明,把保留在原位的節點稱為固定節點。經過這次迴圈的處理,可以看出固定節點是A 和 B。在newChildren中,最靠右的固定節點的位置至關重要,對於後續的移動節點的處理來說,它的意義是提供參考位置。所以,每當處理到最後一個固定節點時,要記住此時它的位置,這個位置就是lastPlacedIndex
。關鍵程式碼如下:
let newIdx = 0;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
...
// 跳出邏輯
...
// 如果不跳出,記錄最新的固定節點的位置
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
...}
placeChild
方法實際上是移動節點的方法,但當節點無需移動的時候,會返回當前節點的位置,對於固定節點來說,因為無需移動,所以返回的就是固定節點的index。
節點刪除
我們沒有提到對刪除節點的處理,實際上刪除節點比較簡單。
舊: A - B - C - D - E
新: A - B - C
因為遍歷的是newChildren,當它遍歷結束,但oldFiber鏈還沒有遍歷完,那麼說明剩下的節點都要被刪除。直接在oldFiber節點上標記Deletion的effectTag來實現刪除。
if (newIdx === newChildren.length) {
// 新子節點遍歷完,說明剩下的oldFiber都是沒用的了,可以刪除
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
deleteRemainingChildren
呼叫了deleteChild
,值得注意的是,刪除不僅僅是標記了effectTag為Deletion,還會將這個被刪除的fiber節點新增到父級的effectList中。
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
...
const last = returnFiber.lastEffect;
// 將要刪除的child新增到父級fiber的effectList中,並新增上effectTag為刪除
if (last !== null) {
last.nextEffect = childToDelete;
returnFiber.lastEffect = childToDelete;
} else {
returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
}
childToDelete.nextEffect = null;
childToDelete.effectTag = Deletion;
}
節點新增
新增節點的場景也很好理解,當oldFiber鏈遍歷完,但newChildren還沒遍歷完,那麼餘下的節點都屬於新插入的節點,會新建fiber節點並以sibling為指標連成fiber鏈。
舊: A - B - C
新: A - B - C - D - E
插入的邏輯(省略了相關度不高的程式碼)
if (oldFiber === null) {
// 舊的遍歷完了,意味著剩下的都是新增的了
for (; newIdx < newChildren.length; newIdx++) { // 首先建立newFiber
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
...
// 再將newFiber連線成以sibling為指標的單向連結串列
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
節點移動
節點的移動是如下場景:
舊 A - B - C - D - E - F
新 A - B - D - C - E
經過第一輪遍歷的處理,固定節點為A B,最新的固定節點的位置(lastPlacedIndex)為1(B的位置)。此時oldFiber鏈中還剩C - D - E - F,newChildren中還剩D - C - E。
接下來的邏輯對於位置不一樣的節點,它自己會先更新再移動。因為此時剩餘的節點位置變了,更新又要複用oldFiber節點,所以為了在更新時方便查詢,會將剩餘的oldFiber節點放入一個以key為鍵,值為oldFiber節點的map中。稱為existingChildren
。
由於newChildren 和 oldFiber節點都沒遍歷完,說明需要移動位置。此刻需要明確一點,就是這些節點都在最新的固定節點的右邊。
移動的邏輯是:newChildren中剩餘的節點,都是不確定要不要移動的,遍歷它們,每一個都去看看這個節點在oldFiber鏈中的位置(舊位置),遍歷到的節點有它在newChildren中的位置(新位置):
如果舊位置在lastPlacedIndex的右邊,說明這個節點位置不變。
原因是舊位置在lastPlacedIndex的右邊,而新節點的位置也在它的右邊,所以它的位置沒變化。因為位置不變,所以它成了固定節點,把lastPlacedIndex更新成新位置。
如果舊位置在lastPlacedIndex的左邊,當前這個節點的位置要往右挪。
原因是舊位置在lastPlacedIndex的左邊,新位置卻在lastPlacedIndex的右邊,所以它要往右挪,但它不是固定節點。此時無需更新lastPlacedIndex。
我們來用上邊的例子過一下這部分邏輯。
舊 A - B - C - D - E - F
新 A - B - D - C - E
位置固定部分 A - B,最右側的固定節點為B,lastPlacedIndex為1
。這時剩餘oldFiber鏈為C - D - E - F,existingChildren為
{
C: '節點C',
D: '節點D',
E: '節點E',
F: '節點F'
}
newChildren的剩餘部分D - C - E繼續遍歷。
首先遍歷到D,D在oldFiber鏈中(A - B - C - D - E)的位置為3
3 > 1
,oldFiber中D的位置在B的右邊,newChildren中也是如此,所以D的位置不動,此時最新的固定節點變成了D
,更新lastPlacedIndex為3
。並從existingChildren中刪除D,
{
C: '節點C',
E: '節點E',
F: '節點F'
}
再遍歷到C,C在oldFiber鏈中(A - B - C - D - E)的索引為2
2 < 3
,C原來在最新固定節點(D
)的左邊,newChildren中C在D
的右邊,所以要給它移動到右邊。並從existingChildren中刪除C。
{
E: '節點E',
F: '節點F'
}
再遍歷到E,E在oldFiber鏈中(A - B - C - D - E)的位置為4
4 > 3
,oldFiber鏈中E位置在D
的位置的右邊,新位置中也是如此,所以E的位置不動,此時最新的固定節點變成了E
,更新lastPlacedIndex為4
。並從existingChildren中刪除E,
{
F: '節點F'
}
這個時候newChildren都處理完了,針對移動節點的遍歷結束。此時還剩一個F節點,是在oldFiber鏈中的,因為newChildren都處理完了,所以將它刪除即可。
existingChildren.forEach(child => deleteChild(returnFiber, child));
可以看到,節點的移動是以最右側的固定節點位置作為參照的。這些固定節點是指位置未發生變化的節點。每次對比節點是否需要移動之後,及時更新固定節點非常重要。
原始碼
瞭解了上邊的多節點diff原理後,將上邊的關鍵點匹配到原始碼上更方便能進一步理解。下面放出帶有詳細註釋的原始碼。
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
): Fiber | null {
/* * returnFiber:currentFirstChild的父級fiber節點
* currentFirstChild:當前執行更新任務的WIP(fiber)節點
* newChildren:元件的render方法渲染出的新的ReactElement節點
* lanes:優先順序相關
* */
// resultingFirstChild是diff之後的新fiber連結串列的第一個fiber。
let resultingFirstChild: Fiber | null = null;
// resultingFirstChild是新連結串列的第一個fiber。
// previousNewFiber用來將後續的新fiber接到第一個fiber之後
let previousNewFiber: Fiber | null = null;
// oldFiber節點,新的child節點會和它進行比較
let oldFiber = currentFirstChild;
// 儲存固定節點的位置
let lastPlacedIndex = 0;
// 儲存遍歷到的新節點的索引
let newIdx = 0;
// 記錄目前遍歷到的oldFiber的下一個節點
let nextOldFiber = null;
// 該輪遍歷來處理節點更新,依據節點是否可複用來決定是否中斷遍歷
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// newChildren遍歷完了,oldFiber鏈沒有遍歷完,此時需要中斷遍歷
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber; oldFiber = null;
} else {
// 用nextOldFiber儲存當前遍歷到的oldFiber的下一個節點
nextOldFiber = oldFiber.sibling;
}
// 生成新的節點,判斷key與tag是否相同就在updateSlot中
// 對DOM型別的元素來說,key 和 tag都相同才會複用oldFiber
// 並返回出去,否則返回null
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
// newFiber為 null說明 key 或 tag 不同,節點不可複用,中斷遍歷
if (newFiber === null) {
if (oldFiber === null) {
// oldFiber 為null說明oldFiber此時也遍歷完了
// 是以下場景,D為新增節點
// 舊 A - B - C
// 新 A - B - C - D oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
// shouldTrackSideEffects 為true表示是更新過程
if (oldFiber && newFiber.alternate === null) {
// newFiber.alternate 等同於 oldFiber.alternate
// oldFiber為WIP節點,它的alternate 就是 current節點
// oldFiber存在,並且經過更新後的新fiber節點它還沒有current節點,
// 說明更新後展現在螢幕上不會有current節點,而更新後WIP
// 節點會稱為current節點,所以需要刪除已有的WIP節點
deleteChild(returnFiber, oldFiber);
}
}
// 記錄固定節點的位置
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 將新fiber連線成以sibling為指標的單向連結串列
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
// 將oldFiber節點指向下一個,與newChildren的遍歷同步移動
oldFiber = nextOldFiber;
}
// 處理節點刪除。新子節點遍歷完,說明剩下的oldFiber都是沒用的了,可以刪除.
if (newIdx === newChildren.length) {
// newChildren遍歷結束,刪除掉oldFiber鏈中的剩下的節點
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 處理新增節點。舊的遍歷完了,能複用的都複用了,所以意味著新的都是新插入的了
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
// 基於新生成的ReactElement建立新的Fiber節點
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
// 記錄固定節點的位置lastPlacedIndex
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 將新生成的fiber節點連線成以sibling為指標的單向連結串列
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// 執行到這是都沒遍歷完的情況,把剩餘的舊子節點放入一個以key為鍵,值為oldFiber節點的map中
// 這樣在基於oldFiber節點新建新的fiber節點時,可以通過key快速地找出oldFiber
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 節點移動
for (; newIdx < newChildren.length; newIdx++) {
// 基於map中的oldFiber節點來建立新fiber
const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes, );
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// 因為newChildren中剩餘的節點有可能和oldFiber節點一樣,只是位置換了,
// 但也有可能是是新增的.
// 如果newFiber的alternate不為空,則說明newFiber不是新增的。
// 也就說明著它是基於map中的oldFiber節點新建的,意味著oldFiber已經被使用了,所以需
// 要從map中刪去oldFiber
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
// 移動節點,多節點diff的核心,這裡真正會實現節點的移動
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 將新fiber連線成以sibling為指標的單向連結串列
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
// 此時newChildren遍歷完了,該移動的都移動了,那麼刪除剩下的oldFiber
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
總結
Diff演算法通過key和tag來對節點進行取捨,可直接將複雜的比對攔截掉,然後降級成節點的移動和增刪這樣比較簡單的操作。對oldFiber和新的ReactElement節點的比對,將會生成新的fiber節點,同時標記上effectTag,這些fiber會被連到workInProgress樹中,作為新的WIP節點。樹的結構因此被一點點地確定,而新的workInProgress節點也基本定型。這意味著,在diff過後,workInProgress節點的beginWork節點就完成了。接下來會進入completeWork階段。
歡迎掃碼關注公眾號,發現更多技術文章