diff算法(媽媽不再擔憂個人diff面試了)

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

8.diff算法(媽媽不再擔憂個人diff面試了)

視頻課程&調試demos

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

​ 視頻課程:進入課程面試

​ 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過程的主要流程以下圖:markdown

_14

​ 咱們知道對比兩顆樹的複雜度自己是O(n3),對咱們的應用來講這個是不能承受的量級,react爲了下降複雜度,提出了三個前提:併發

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

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

  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;
      }
    複製代碼
相關文章
相關標籤/搜索