兩種節點型別
我們可以從同級的節點數量將Diff分為兩類:
當newChild型別為object、number、string,代表同級只有一個節點
當newChild型別為Array,同級有多個節點
在接下來兩節我們會分別討論這兩類節點的Diff,注意這裡的單節點是指虛擬dom節點是個單或者多節點,可以簡單看做是不是返回的陣列
單節點
單節點比較還是比較簡單的
//刪除節點
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
if (!shouldTrackSideEffects) {
// Noop.
return;
}
//effect鏈的處理
const last = returnFiber.lastEffect;
if (last !== null) {
last.nextEffect = childToDelete;
returnFiber.lastEffect = childToDelete;
} else {
//證明暫時還沒有形成鏈需要第一個節點
returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
}
const deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [childToDelete];
// TODO (effects) Rename this to better reflect its new usage (e.g. ChildDeletions)
returnFiber.effectTag |= Deletion;
} else {
deletions.push(childToDelete);
}
childToDelete.nextEffect = null;
}
//批次刪除節點的工具函式(更準確的是批次標記)
function deleteRemainingChildren(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
): null {
if (!shouldTrackSideEffects) {
// Noop.
return null;
}
// TODO: For the shouldClone case, this could be micro-optimized a bit by
// assuming that after the first child we've already added everything.
let childToDelete = currentFirstChild;
while (childToDelete !== null) {
deleteChild(returnFiber, childToDelete);
childToDelete = childToDelete.sibling;
}
return null;
}
//element其實就是新的虛擬dom
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement
): Fiber {
const key = element.key;
let child = currentFirstChild;
// 首先判斷是否存在對應DOM節點
while (child !== null) {
// 上一次更新存在DOM節點,接下來判斷是否可複用
// 首先比較key是否相同
if (child.key === key) {
// key相同,接下來比較type是否相同
switch (child.tag) {
// ...省略case
default: {
if (child.elementType === element.type) {
// type相同則表示可以複用
deleteRemainingChildren(returnFiber, child.sibling);//顯然這個節點的後續節點都必須刪除了 因為找到了
const existing = useFiber(child, element.props);//useFiber故名思義 這裡的element.props就是後續看是否要調整的屬性
// 返回複用的fiber
return existing;
}
// type不同則跳出switch
break;
}
}
// 程式碼執行到這裡代表:key相同但是type不同
// 將該fiber及其兄弟fiber標記為刪除
deleteRemainingChildren(returnFiber, child);
break;
} else {
// key不同,將該fiber標記為刪除
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 建立新Fiber,並返回 ...省略
}
可以發現需要被刪除的fiber 不會在這直接真的刪除,而是形成一個effect鏈,另外父節點會維護一個deletions的fiber陣列
首先判斷child是否存在,不存在則直接開始兄弟節點的比較,while終止在同層比較完成後
幾種邏輯分支
- key相同,型別也相同直接可複用,後續就看屬性情況更新屬性即可
- key相同,型別不同了,直接deleteRemainingChildren 刪除這個節點及他的兄弟節點,這裡是因為key相同了,後續沒有繼續比較找可複用節點的意義了,故把原節點刪完就可以了
- key不同,直接把這個比較的節點刪除
多節點
先整理一下原始碼
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
): Fiber | null {
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
// need to delete the existing child.
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
//構建新的fiber鏈作為返回值
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {//構建新的fiber鏈作為返回值
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// Add all children to a key map for quick lookups.
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Keep scanning and use the map to restore deleted items as moves.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
先對幾個用到的重要函式解讀一下
updateSlot這個函式可以簡單理解為節點比較,如果不匹配返回null,不然就是一個可複用的fiber節點
function mapRemainingChildren(
returnFiber: Fiber,
currentFirstChild: Fiber,
): Map<string | number, Fiber> {
const existingChildren: Map<string | number, Fiber> = new Map();
let existingChild = currentFirstChild;
while (existingChild !== null) {
if (existingChild.key !== null) {
existingChildren.set(existingChild.key, existingChild);
} else {
existingChildren.set(existingChild.index, existingChild);
}
existingChild = existingChild.sibling;
}
return existingChildren;
}
mapRemainingChildren返回一個children構成的Map key為id或者是child的index
內容比較多建議分成三個步驟去看
第一個迴圈,比較newChildren 和oldFiber和他的兄弟們 會有三種情況
- key不同迴圈直接停止
- key相同,型別不同,fiber標記為刪除迴圈繼續 i++ oldFiber = nextOldFiber;
- 迴圈結束newChildren或者是oldFiber和他的兄弟們遍歷結束了
- 迴圈完處理一下幾種情況 ,一種是newChildren現遍歷完了,那刪除剩餘的oldFiber,deleteRemainingChildren(returnFiber, oldFiber); 第二種是oldFiber遍歷完了,那剩餘的newChildren 需要建立fiber節點 並且拼接在previousNewFiber這個結果鏈上 觸發這兩種情況都會退出整個diff
- 也就是都沒有遍歷完,情況就是由於節點位置移動導致的,這個時候先要mapRemainingChildren(returnFiber, oldFiber);把剩餘的fiber做一個Map對映,然後newChildren 剩餘的節點去Map中查詢,重點是placeChild函式
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
// Noop.
return lastPlacedIndex;
}
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// This is a move.
//原本節點的index比當前最近一次替換過的節點的index還小的話標記為移動,且lastPlacedIndex不變
newFiber.effectTag = Placement;
return lastPlacedIndex;
} else {
// This item can stay in place.
//返回原有節點的位置作為新的lastPlacedIndex
return oldIndex;
}
} else {
// This is an insertion.
//newChildren在原本沒有 完全是新建的
newFiber.effectTag = Placement;
return lastPlacedIndex;
}
}
舉個例子如果 01234要變為12304 假設lastPlacedIndex為0初始開始迴圈
1 oldIndex為1 oldIndex > lastPlacedIndex 不動 lastPlacedIndex = oldIndex也就是1
2 oldIndex為2 oldIndex > lastPlacedIndex 不動 lastPlacedIndex = oldIndex也就是2
3 oldIndex為3 oldIndex > lastPlacedIndex 不動 lastPlacedIndex = oldIndex也就是3
0 oldIndex為0 oldIndex < lastPlacedIndex 標記移動 lastPlacedIndex 不變還是3
4 oldIndex為4 oldIndex > lastPlacedIndex 不動 lastPlacedIndex 改為4