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;
      }
    复制代码