react源码解析9.diff算法

视频解说(高效学习):进入学习

往期文章:

1.开篇介绍和面试题

2.react的设计理念

3.react源码架构

4.源码目录构造和调试

5.jsx&外围api

6.legacy和concurrent模式入口函数

7.Fiber架构

8.render阶段

9.diff算法

10.commit阶段

11.生命周期

12.状态更新流程

13.hooks源码

14.手写hooks

15.scheduler&Lane

16.concurrent模式

17.context

18事件零碎

19.手写迷你版react

20.总结&第一章的面试题解答

在render阶段更新Fiber节点时,咱们会调用reconcileChildFibers比照current Fiber和jsx对象构建workInProgress Fiber,这里current Fiber是指以后dom对应的fiber树,jsx是class组件render办法或者函数组件的返回值。

在reconcileChildFibers中会依据newChild的类型来进入单节点的diff或者多节点diff

//ReactChildFiber.old.jsfunction 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过程的次要流程如下图:

咱们晓得比照两颗树的复杂度自身是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有三个for循环遍历(并不意味着所有更新都有经验三个遍历,进入循环体有条件,也有条件跳出循环),第一个遍历解决节点的更新(包含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过程完结

      1. newChildren没遍历完,oldFiber遍历完,将剩下的newChildren的节点标记为Placement,即插入的Tag

        1. newChildren和oldFiber没遍历完,则进入节点挪动的逻辑
  • 第三个遍历
    次要逻辑在placeChild函数中,例如更新前节点程序是ABCD,更新后是ACDB

    1. newChild中第一个地位的A和oldFiber第一个地位的A,key雷同可复用,lastPlacedIndex=0

      1. newChild中第二个地位的C和oldFiber第二个地位的B,key不同跳出第一次循环,将oldFiber中的BCD保留在map中
      1. newChild中第二个地位的C在oldFiber中的index=2 > lastPlacedIndex=0不须要挪动,lastPlacedIndex=2
      2. newChild中第三个地位的D在oldFiber中的index=3 > lastPlacedIndex=2不须要挪动,lastPlacedIndex=3
      3. newChild中第四个地位的B在oldFiber中的index=1 < lastPlacedIndex=3,挪动到最初

    看图更直观

    例如更新前节点程序是ABCD,更新后是DABC

    1. newChild中第一个地位的D和oldFiber第一个地位的A,key不雷同不可复用,将oldFiber中的ABCD保留在map中,lastPlacedIndex=0

      1. 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,挪动到最初

    看图更直观

代码如下

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