react源碼解析9.diff算法

react源碼解析9.diff算法

視頻課程(高效學(xué)習(xí)):進入課程

課程目錄:

1.開篇介紹和面試題

2.react的設(shè)計理念

3.react源碼架構(gòu)

4.源碼目錄結(jié)構(gòu)和調(diào)試

5.jsx&核心api

6.legacy和concurrent模式入口函數(shù)

7.Fiber架構(gòu)

8.render階段

9.diff算法

10.commit階段

11.生命周期

12.狀態(tài)更新流程

13.hooks源碼

14.手寫hooks

15.scheduler&Lane

16.concurrent模式

17.context

18事件系統(tǒng)

19.手寫迷你版react

20.總結(jié)&第一章的面試題解答

21.demo

在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

//ReactChildFiber.old.js
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過程的主要流程如下圖:

react源碼9.5

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

  1. 只對同級比較沽瘦,跨層級的dom不會進行復(fù)用

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

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

    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é)點不同盛卡,導(dǎo)致他們都不能復(fù)用,所以會銷毀之前的節(jié)點筑凫,并新建節(jié)點滑沧,但是現(xiàn)在有key了,b中的節(jié)點會在老的a中尋找key相同的節(jié)點嘗試復(fù)用巍实,最后發(fā)現(xiàn)只是交換位置就可以完成更新滓技,具體對比過程后面會講到。

單節(jié)點diff

單點diff有如下幾種情況:

  • key和type相同表示可以復(fù)用節(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相同則可以復(fù)用 返回復(fù)用的節(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比較復(fù)雜洗显,我們分三種情況進行討論,其中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有三個for循環(huán)遍歷(并不意味著所有更新都有經(jīng)歷三個遍歷挠唆,進入循環(huán)體有條件,也有條件跳出循環(huán))嘱吗,第一個遍歷處理節(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相同則可以復(fù)用

      newChildren遍歷完趴荸,oldFiber沒遍歷完,在第一次遍歷完成之后將oldFiber中沒遍歷完的節(jié)點標記為DELETION宦焦,即刪除的DELETION Tag

  • 第二個遍歷
    第二個遍歷考慮三種情況

    1. newChildren和oldFiber都遍歷完:多節(jié)點diff過程結(jié)束

    2. newChildren沒遍歷完发钝,oldFiber遍歷完,將剩下的newChildren的節(jié)點標記為Placement波闹,即插入的Tag

    3. newChildren和oldFiber沒遍歷完酝豪,則進入節(jié)點移動的邏輯

  • 第三個遍歷
    主要邏輯在placeChild函數(shù)中,例如更新前節(jié)點順序是ABCD精堕,更新后是ACDB

    1. newChild中第一個位置的A和oldFiber第一個位置的A孵淘,key相同可復(fù)用,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

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

    看圖更直觀

    react源碼9.6

    例如更新前節(jié)點順序是ABCD背捌,更新后是DABC

    1. newChild中第一個位置的D和oldFiber第一個位置的A,key不相同不可復(fù)用洞斯,將oldFiber中的ABCD保存在map中毡庆,lastPlacedIndex=0

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

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

    看圖更直觀

    react源碼9.7

    代碼如下

//ReactChildFiber.old.js

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;
       }
     }
//ReactChildFiber.old.js

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;//上次可復(fù)用的節(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閱讀 212,294評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刀闷,死亡現(xiàn)場離奇詭異熊泵,居然都是意外死亡,警方通過查閱死者的電腦和手機甸昏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,493評論 3 385
  • 文/潘曉璐 我一進店門顽分,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人施蜜,你說我怎么就攤上這事卒蘸。” “怎么了?”我有些...
    開封第一講書人閱讀 157,790評論 0 348
  • 文/不壞的土叔 我叫張陵缸沃,是天一觀的道長恰起。 經(jīng)常有香客問我,道長趾牧,這世上最難降的妖魔是什么检盼? 我笑而不...
    開封第一講書人閱讀 56,595評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮翘单,結(jié)果婚禮上吨枉,老公的妹妹穿的比我還像新娘。我一直安慰自己哄芜,他們只是感情好貌亭,可當我...
    茶點故事閱讀 65,718評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著认臊,像睡著了一般圃庭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上失晴,一...
    開封第一講書人閱讀 49,906評論 1 290
  • 那天剧腻,我揣著相機與錄音,去河邊找鬼涂屁。 笑死恕酸,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的胯陋。 我是一名探鬼主播蕊温,決...
    沈念sama閱讀 39,053評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼遏乔!你這毒婦竟也來了义矛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,797評論 0 268
  • 序言:老撾萬榮一對情侶失蹤盟萨,失蹤者是張志新(化名)和其女友劉穎凉翻,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捻激,經(jīng)...
    沈念sama閱讀 44,250評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡涎拉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,570評論 2 327
  • 正文 我和宋清朗相戀三年嘹吨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,711評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡店归,死狀恐怖挂脑,靈堂內(nèi)的尸體忽然破棺而出罗售,到底是詐尸還是另有隱情判族,我是刑警寧澤伶棒,帶...
    沈念sama閱讀 34,388評論 4 332
  • 正文 年R本政府宣布,位于F島的核電站彩库,受9級特大地震影響肤无,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜骇钦,卻給世界環(huán)境...
    茶點故事閱讀 40,018評論 3 316
  • 文/蒙蒙 一宛渐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧眯搭,春花似錦皇忿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,796評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽叨襟。三九已至繁扎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間糊闽,已是汗流浹背梳玫。 一陣腳步聲響...
    開封第一講書人閱讀 32,023評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留右犹,地道東北人提澎。 一個月前我還...
    沈念sama閱讀 46,461評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像念链,于是被迫代替她去往敵國和親盼忌。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,595評論 2 350

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