react中diff算法(面试必备)

react中diff算法(面试必备)

视频课程:进入课程

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](https://upload-images.jianshu.io/upload_images/25882458-b66dd2650a47f867.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

 

 **代码如下**:

 ```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的位置 则将节点插入到最后
         newFiber.flags = Placement;
         return lastPlacedIndex;
       } else {
         return oldIndex;//不需要移动 lastPlacedIndex = oldIndex;
       }
     } else {
       //新增插入
       newFiber.flags = Placement;
       return lastPlacedIndex;
     }
   }
 ```

    

 ```js
 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;
   }
 ```
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容