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

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

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

視頻課程&調(diào)試demos

視頻課程的目的是為了快速掌握react源碼運行的過程和react中的scheduler沛豌、reconciler发魄、renderer泰偿、fiber等,并且詳細debug源碼和分析铸豁,過程更清晰灌曙。

視頻課程:進入課程

demos:demo

課程結(jié)構(gòu):

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

在render階段更新Fiber節(jié)點時节芥,我們會調(diào)用reconcileChildFibers對比current Fiber和jsx對象構(gòu)建workInProgress Fiber在刺,這里current Fiber是指當前dom對應(yīng)的fiber樹,jsx是class組件render方法或者函數(shù)組件的返回值头镊。

在reconcileChildFibers中會根據(jù)newChild的類型來進入單節(jié)點的diff或者多節(jié)點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:
                //單一節(jié)點diff
        return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
    }
  }
    //...

  if (isArray(newChild)) {
     //多節(jié)點diff
    return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
  }

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

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

_14

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

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

  2. 不同類型節(jié)點生成的dom樹不同,此時會直接銷毀老節(jié)點及子孫節(jié)點坛芽,并新建節(jié)點

  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,因為節(jié)點的更新前后文本節(jié)點不同咙轩,導致他們都不能復用获讳,所以會銷毀之前的節(jié)點,并新建節(jié)點活喊,但是現(xiàn)在有key了丐膝,b中的節(jié)點會在老的a中尋找key相同的節(jié)點嘗試復用,最后發(fā)現(xiàn)只是交換位置就可以完成更新钾菊,具體對比過程后面會講到帅矗。

    單節(jié)點diff

    單點diff有如下幾種情況:

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

    多節(jié)點diff

    多節(jié)點diff比較復雜红竭,我們分三種情況進行討論尤勋,其中a表示更新前的節(jié)點,b表示更新后的節(jié)點

    • 屬性變化

      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>
          </>
        );
      
      
    • 新增節(jié)點

      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>
          </>
        );    
      
      
    • 節(jié)點刪除

      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>
          </>
        );
      
      
    • 節(jié)點位置變化

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

    在源碼中多節(jié)點diff會經(jīng)歷三次遍歷茵宪,第一次遍歷處理節(jié)點的更新(包括props更新和type更新和刪除)最冰,第二次遍歷處理其他的情況(節(jié)點新增),其原因在于在大多數(shù)的應(yīng)用中稀火,節(jié)點更新的頻率更加頻繁暖哨,第三次處理位節(jié)點置改變

    • 第一次遍歷

      因為老的節(jié)點存在于current Fiber中,所以它是個鏈表結(jié)構(gòu)凰狞,還記得Fiber雙緩存結(jié)構(gòu)嘛篇裁,節(jié)點通過child沛慢、return、sibling連接达布,而newChildren存在于jsx當中团甲,所以遍歷對比的時候,首先讓newChildren[i]oldFiber對比黍聂,然后讓i++躺苦、nextOldFiber = oldFiber.sibling。在第一輪遍歷中产还,會處理三種情況匹厘,其中第1,2兩種情況會結(jié)束第一次循環(huán)

      1. key不同脐区,第一次循環(huán)結(jié)束
      2. newChildren或者oldFiber遍歷完愈诚,第一次循環(huán)結(jié)束
      3. key同type不同,標記oldFiber為DELETION
      4. key相同type相同則可以復用

      newChildren遍歷完坡椒,oldFiber沒遍歷完扰路,在第一次遍歷完成之后將oldFiber中沒遍歷完的節(jié)點標記為DELETION,即刪除的DELETION Tag

  • 第二次遍歷

    第二次遍歷考慮三種情況

      1\. newChildren和oldFiber都遍歷完:多節(jié)點diff過程結(jié)束
      2\. newChildren沒遍歷完倔叼,oldFiber遍歷完汗唱,將剩下的newChildren的節(jié)點標記為Placement,即插入的Tag
    
    
    1. newChildren和oldFiber沒遍歷完丈攒,則進入節(jié)點移動的邏輯
  • 第三次遍歷

    主要邏輯在placeChild函數(shù)中哩罪,例如更新前節(jié)點順序是ABCD,更新后是ACDB

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

    2. newChild中第二個位置的C和oldFiber第二個位置的B,key不同跳出第一次循環(huán)显设,將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

    例如更新前節(jié)點順序是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的位置 則將節(jié)點插入到最后
            newFiber.flags = Placement;
            return lastPlacedIndex;
          } else {
            return oldIndex;//不需要移動 lastPlacedIndex = oldIndex;
          }
        } else {
          //新增插入
          newFiber.flags = Placement;
          return lastPlacedIndex;
        }
      }
    
    
```
function reconcileChildrenArray(
    returnFiber: Fiber,//父fiber節(jié)點
    currentFirstChild: Fiber | null,//childs中第一個節(jié)點
    newChildren: Array<*>,//新節(jié)點數(shù)組 也就是jsx數(shù)組
    lanes: Lanes,//lane相關(guān) 第12章介紹
  ): Fiber | null {

    let resultingFirstChild: Fiber | null = null;//diff之后返回的第一個節(jié)點
    let previousNewFiber: Fiber | null = null;//新節(jié)點中上次對比過的節(jié)點

    let oldFiber = currentFirstChild;//正在對比的oldFiber
    let lastPlacedIndex = 0;//上次可復用的節(jié)點位置 或者oldFiber的位置
    let newIdx = 0;//新節(jié)點中對比到了的位置
    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(//更新節(jié)點,如果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);//標記節(jié)點插入
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    if (newIdx === newChildren.length) {
      deleteRemainingChildren(returnFiber, oldFiber);//將oldFiber中沒遍歷完的節(jié)點標記為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);//插入新增節(jié)點
        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++) {//第三次循環(huán) 處理節(jié)點移動
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) {
            existingChildren.delete(//刪除找到的節(jié)點
              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中剩下的節(jié)點
      existingChildren.forEach(child => deleteChild(returnFiber, child));
    }

    return resultingFirstChild;
  }

```
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末允悦,一起剝皮案震驚了整個濱河市膝擂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖架馋,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件狞山,死亡現(xiàn)場離奇詭異,居然都是意外死亡叉寂,警方通過查閱死者的電腦和手機铣墨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來办绝,“玉大人,你說我怎么就攤上這事姚淆≡胁酰” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵腌逢,是天一觀的道長降淮。 經(jīng)常有香客問我,道長搏讶,這世上最難降的妖魔是什么佳鳖? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮媒惕,結(jié)果婚禮上系吩,老公的妹妹穿的比我還像新娘。我一直安慰自己妒蔚,他們只是感情好穿挨,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著肴盏,像睡著了一般科盛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上菜皂,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天贞绵,我揣著相機與錄音,去河邊找鬼恍飘。 笑死榨崩,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的常侣。 我是一名探鬼主播蜡饵,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼胳施!你這毒婦竟也來了溯祸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎焦辅,沒想到半個月后博杖,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡筷登,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年剃根,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片前方。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡狈醉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出惠险,到底是詐尸還是另有隱情苗傅,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布班巩,位于F島的核電站渣慕,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏抱慌。R本人自食惡果不足惜逊桦,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望抑进。 院中可真熱鬧强经,春花似錦、人聲如沸单匣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽户秤。三九已至码秉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鸡号,已是汗流浹背转砖。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鲸伴,地道東北人府蔗。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像汞窗,于是被迫代替她去往敵國和親姓赤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 文章首發(fā)于個人博客 這是我 Deep In React 系列的第二篇文章仲吏,如果還沒有讀過的強烈建議你先讀第一篇:詳...
    勿忘巛心安閱讀 1,050評論 1 2
  • 我相信在看這篇文章的讀者一般都已經(jīng)了解過 React 16 以前的 Diff 算法了不铆,這個算法也算是 React ...
    前端_java愛好者閱讀 967評論 1 0
  • css相關(guān) 1. 萬能居中 1.margin: 0 auto;水平 2.text-align: center;水平...
    chaocc閱讀 964評論 0 2
  • 1.關(guān)于閉包 什么是閉包蝌焚? 閉包是有權(quán)限訪問其它函數(shù)作用域內(nèi)的變量的一個函數(shù)。 在js中誓斥,變量分為全局變量和局部變...
    自律寶藏男孩閱讀 308評論 0 0
  • 推薦指數(shù): 6.0 書籍主旨關(guān)鍵詞:特權(quán)只洒、焦點、注意力劳坑、語言聯(lián)想毕谴、情景聯(lián)想 觀點: 1.統(tǒng)計學現(xiàn)在叫數(shù)據(jù)分析,社會...
    Jenaral閱讀 5,721評論 0 5