react fiber 中的dom diff

求實亭下發表於2023-02-15

兩種節點型別

我們可以從同級的節點數量將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終止在同層比較完成後
幾種邏輯分支

  1. key相同,型別也相同直接可複用,後續就看屬性情況更新屬性即可
  2. key相同,型別不同了,直接deleteRemainingChildren 刪除這個節點及他的兄弟節點,這裡是因為key相同了,後續沒有繼續比較找可複用節點的意義了,故把原節點刪完就可以了
  3. 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

內容比較多建議分成三個步驟去看

  1. 第一個迴圈,比較newChildren 和oldFiber和他的兄弟們 會有三種情況

    • key不同迴圈直接停止
    • key相同,型別不同,fiber標記為刪除迴圈繼續 i++ oldFiber = nextOldFiber;
    • 迴圈結束newChildren或者是oldFiber和他的兄弟們遍歷結束了
  2. 迴圈完處理一下幾種情況 ,一種是newChildren現遍歷完了,那刪除剩餘的oldFiber,deleteRemainingChildren(returnFiber, oldFiber); 第二種是oldFiber遍歷完了,那剩餘的newChildren 需要建立fiber節點 並且拼接在previousNewFiber這個結果鏈上 觸發這兩種情況都會退出整個diff
  3. 也就是都沒有遍歷完,情況就是由於節點位置移動導致的,這個時候先要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

相關文章