8.diff算法(媽媽再也不擔心我的diff面試了)


人人都能讀懂的react源碼解析(大廠高薪必備)

8.diff算法(媽媽再也不擔心我的diff面試了)

視頻課程&調試demos

​ 視頻課程的目的是為了快速掌握react源碼運行的過程和react中的scheduler、reconciler、renderer、fiber等,並且詳細debug源碼和分析,過程更清晰。

​ 視頻課程:進入課程

​ demos:demo

課程結構:

  1. 開篇(聽說你還在艱難的啃react源碼)
  2. react心智模型(來來來,讓大腦有react思維吧)
  3. Fiber(我是在內存中的dom)
  4. 從legacy或concurrent開始(從入口開始,然后讓我們奔向未來)
  5. state更新流程(setState里到底發生了什么)
  6. render階段(厲害了,我有創建Fiber的技能)
  7. commit階段(聽說renderer幫我們打好標記了,映射真實節點吧)
  8. diff算法(媽媽再也不擔心我的diff面試了)
  9. hooks源碼(想知道Function Component是怎樣保存狀態的嘛)
  10. scheduler&lane模型(來看看任務是暫停、繼續和插隊的)
  11. concurrent mode(並發模式是什么樣的)
  12. 手寫迷你react(短小精悍就是我)

​ 在render階段更新Fiber節點時,我們會調用reconcileChildFibers對比current Fiber和jsx對象構建workInProgress Fiber,這里current Fiber是指當前dom對應的fiber樹,jsx是class組件render方法或者函數組件的返回值。

​ 在reconcileChildFibers中會根據newChild的類型來進入單節點的diff或者多節點diff

function reconcileChildFibers(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChild: any,
): Fiber | null {

  const isObject = typeof newChild === 'object' && newChild !== null;

  if (isObject) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
				//單一節點diff
        return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
    }
  }
	//...
  
  if (isArray(newChild)) {
     //多節點diff
    return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
  }

  // 刪除節點
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}

diff過程的主要流程如下圖:

_14

​ 我們知道對比兩顆樹的復雜度本身是O(n3),對我們的應用來說這個是不能承受的量級,react為了降低復雜度,提出了三個前提:

  1. 只對同級比較,跨層級的dom不會進行復用

  2. 不同類型節點生成的dom樹不同,此時會直接銷毀老節點及子孫節點,並新建節點

  3. 可以通過key來對元素diff的過程提供復用的線索,例如:

    const a = (
        <>
          <p key="0">0</p>
          <p key="1">1</p>
        </>
      );
    const b = (
      <>
        <p key="1">1</p>
        <p key="0">0</p>
      </>
    );
    

    ​ 如果a和b里的元素都沒有key,因為節點的更新前后文本節點不同,導致他們都不能復用,所以會銷毀之前的節點,並新建節點,但是現在有key了,b中的節點會在老的a中尋找key相同的節點嘗試復用,最后發現只是交換位置就可以完成更新,具體對比過程后面會講到。

    單節點diff

    單點diff有如下幾種情況:

    • key和type相同表示可以復用節點
    • key不同直接標記刪除節點,然后新建節點
    • key相同type不同,標記刪除該節點和兄弟節點,然后新創建節點
    function reconcileSingleElement(
      returnFiber: Fiber,
      currentFirstChild: Fiber | null,
      element: ReactElement
    ): Fiber {
      const key = element.key;
      let child = currentFirstChild;
      
      //child節點不為null執行對比
      while (child !== null) {
    
        // 1.比較key
        if (child.key === key) {
    
          // 2.比較type
    
          switch (child.tag) {
            //...
            
            default: {
              if (child.elementType === element.type) {
                // type相同則可以復用 返回復用的節點
                return existing;
              }
              // type不同跳出
              break;
            }
          }
          //key相同,type不同則把fiber及和兄弟fiber標記刪除
          deleteRemainingChildren(returnFiber, child);
          break;
        } else {
          //key不同直接標記刪除該節點
          deleteChild(returnFiber, child);
        }
        child = child.sibling;
      }
       
      //新建新Fiber
    }
    
    

    多節點diff

    多節點diff比較復雜,我們分三種情況進行討論,其中a表示更新前的節點,b表示更新后的節點

    • 屬性變化

      const a = (
          <>
            <p key="0" name='0'>0</p>
            <p key="1">1</p>
          </>
        );
        const b = (
          <>
            <p key="0" name='00'>0</p>
            <p key="1">1</p>
          </>
        );
      
    • type變化

      const a = (
          <>
            <p key="0">0</p>
            <p key="1">1</p>
          </>
        );
        const b = (
          <>
            <div key="0">0</div>
            <p key="1">1</p>
          </>
        );
      
    • 新增節點

      const a = (
          <>
            <p key="0">0</p>
            <p key="1">1</p>
          </>
        );
        const b = (
          <>
            <p key="0">0</p>
            <p key="1">1</p>
            <p key="2">2</p>
          </>
        );	
      
    • 節點刪除

      const a = (
          <>
            <p key="0">0</p>
            <p key="1">1</p>
            <p key="2">2</p>
          </>
        );
        const b = (
          <>
            <p key="0">0</p>
            <p key="1">1</p>
          </>
        );
      
    • 節點位置變化

      	const a = (
          <>
            <p key="0">0</p>
            <p key="1">1</p>
          </>
        );
        const b = (
          <>
            <p key="1">1</p>
            <p key="0">0</p>
          </>
        );
      

    ​ 在源碼中多節點diff會經歷三次遍歷,第一次遍歷處理節點的更新(包括props更新和type更新和刪除),第二次遍歷處理其他的情況(節點新增),其原因在於在大多數的應用中,節點更新的頻率更加頻繁,第三次處理位節點置改變

    • 第一次遍歷

      ​ 因為老的節點存在於current Fiber中,所以它是個鏈表結構,還記得Fiber雙緩存結構嘛,節點通過child、return、sibling連接,而newChildren存在於jsx當中,所以遍歷對比的時候,首先讓newChildren[i]oldFiber對比,然后讓i++、nextOldFiber = oldFiber.sibling。在第一輪遍歷中,會處理三種情況,其中第1,2兩種情況會結束第一次循環

      1. key不同,第一次循環結束
      2. newChildren或者oldFiber遍歷完,第一次循環結束
      3. key同type不同,標記oldFiber為DELETION
      4. key相同type相同則可以復用

      newChildren遍歷完,oldFiber沒遍歷完,在第一次遍歷完成之后將oldFiber中沒遍歷完的節點標記為DELETION,即刪除的DELETION Tag

  • 第二次遍歷

    第二次遍歷考慮三種情況

      1. newChildren和oldFiber都遍歷完:多節點diff過程結束
      2. newChildren沒遍歷完,oldFiber遍歷完,將剩下的newChildren的節點標記為Placement,即插入的Tag
    
    1. newChildren和oldFiber沒遍歷完,則進入節點移動的邏輯
  • 第三次遍歷

    主要邏輯在placeChild函數中,例如更新前節點順序是ABCD,更新后是ACDB

    1. newChild中第一個位置的A和oldFiber第一個位置的A,key相同可復用,lastPlacedIndex=0

    2. newChild中第二個位置的C和oldFiber第二個位置的B,key不同跳出第一次循環,將oldFiber中的BCD保存在map中

    3. newChild中第二個位置的C在oldFiber中的index=2 > lastPlacedIndex=0不需要移動,lastPlacedIndex=2

    4. newChild中第三個位置的D在oldFiber中的index=3 > lastPlacedIndex=2不需要移動,lastPlacedIndex=3

      1. newChild中第四個位置的B在oldFiber中的index=1 < lastPlacedIndex=3,移動到最后

        看圖更直觀

      _15

    例如更新前節點順序是ABCD,更新后是DABC

    1. newChild中第一個位置的D和oldFiber第一個位置的A,key不相同不可復用,將oldFiber中的ABCD保存在map中,lastPlacedIndex=0

    2. newChild中第一個位置的D在oldFiber中的index=3 > lastPlacedIndex=0不需要移動,lastPlacedIndex=3

    3. newChild中第二個位置的A在oldFiber中的index=0 < lastPlacedIndex=3,移動到最后

    4. newChild中第三個位置的B在oldFiber中的index=1 < lastPlacedIndex=3,移動到最后

    5. newChild中第四個位置的C在oldFiber中的index=2 < lastPlacedIndex=3,移動到最后

      看圖更直觀

    _16

    代碼如下

      function placeChild(newFiber, lastPlacedIndex, newIndex) {
        newFiber.index = newIndex;
    
        if (!shouldTrackSideEffects) {
          return lastPlacedIndex;
        }
    
     var current = newFiber.alternate;
    
        if (current !== null) {
          var oldIndex = current.index;
    
          if (oldIndex < lastPlacedIndex) {
            //oldIndex小於lastPlacedIndex的位置 則將節點插入到最后
            newFiber.flags = Placement;
            return lastPlacedIndex;
          } else {
            return oldIndex;//不需要移動 lastPlacedIndex = oldIndex;
          }
        } else {
          //新增插入
          newFiber.flags = Placement;
          return lastPlacedIndex;
        }
      }
    

    function reconcileChildrenArray(
        returnFiber: Fiber,//父fiber節點
        currentFirstChild: Fiber | null,//childs中第一個節點
        newChildren: Array<*>,//新節點數組 也就是jsx數組
        lanes: Lanes,//lane相關 第12章介紹
      ): Fiber | null {
    
        let resultingFirstChild: Fiber | null = null;//diff之后返回的第一個節點
        let previousNewFiber: Fiber | null = null;//新節點中上次對比過的節點
    
        let oldFiber = currentFirstChild;//正在對比的oldFiber
        let lastPlacedIndex = 0;//上次可復用的節點位置 或者oldFiber的位置
        let newIdx = 0;//新節點中對比到了的位置
        let nextOldFiber = null;//正在對比的oldFiber
        for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {//第一次遍歷
          if (oldFiber.index > newIdx) {//nextOldFiber賦值
            nextOldFiber = oldFiber;
            oldFiber = null;
          } else {
            nextOldFiber = oldFiber.sibling;
          }
          const newFiber = updateSlot(//更新節點,如果key不同則newFiber=null
            returnFiber,
            oldFiber,
            newChildren[newIdx],
            lanes,
          );
          if (newFiber === null) {
            if (oldFiber === null) {
              oldFiber = nextOldFiber;
            }
            break;//跳出第一次遍歷
          }
          if (shouldTrackSideEffects) {//檢查shouldTrackSideEffects
            if (oldFiber && newFiber.alternate === null) {
              deleteChild(returnFiber, oldFiber);
            }
          }
          lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//標記節點插入
          if (previousNewFiber === null) {
            resultingFirstChild = newFiber;
          } else {
            previousNewFiber.sibling = newFiber;
          }
          previousNewFiber = newFiber;
          oldFiber = nextOldFiber;
        }
    
        if (newIdx === newChildren.length) {
          deleteRemainingChildren(returnFiber, oldFiber);//將oldFiber中沒遍歷完的節點標記為DELETION
          return resultingFirstChild;
        }
    
        if (oldFiber === null) {
          for (; newIdx < newChildren.length; newIdx++) {//第2次遍歷
            const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
            if (newFiber === null) {
              continue;
            }
            lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//插入新增節點
            if (previousNewFiber === null) {
              resultingFirstChild = newFiber;
            } else {
              previousNewFiber.sibling = newFiber;
            }
            previousNewFiber = newFiber;
          }
          return resultingFirstChild;
        }
    
        // 將剩下的oldFiber加入map中
        const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    
        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) {
          //刪除existingChildren中剩下的節點
          existingChildren.forEach(child => deleteChild(returnFiber, child));
        }
    
        return resultingFirstChild;
      }
    


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM