diff发生在render阶段(“递”和“归“)
递: 从顶层节点,向下遍历(深度优先),有子节点就遍历子节点,没有再查询兄弟节点
归: 如果没有子节点就进行当前节点的“归操作”,然后查询是否有兄弟节点,没有进行当前节点父节点的“归”过程,有兄弟节点进行兄弟节点的“递”。
递归交错直到归到root节点,diff就发生在递归的阶段

const App = () => {

  return (
    <div>
      <span>child text</span>
      <div>test</div>
    </div>
  );
};

上面的代码对应的树结构:
image.png

diff实现

单一节点对比(新节点是object,number,string。比如:如果原来的节点是list多个节点,但是更新之后是单一节点也按照单一对比进行)

  • 有key的情况下,只有key和类型都相同才会复用
  • 有key,key相同并且类型不同,直接删除不能复用并且要是旧节点有兄弟节点也不用再参与对比都标记删除,因为已经通过key匹配到旧节点中可能可以复用的节点
  • 有key,key不同,标记当前对比的节点为删除,要是当前节点有兄弟节点就继续遍历查询,没有可以复用的就标记删除
  • 没有key,如果类型不同直接替换
  • 没有key,类型相同,继续比较属性(props)及子节点,属性比较,属性不同直接更新属性
  • 文本:如果是文本节点直接对比内容

单一节点的例子:

// 旧
<div>
  <div key="1">1</div>
  <div key="2">2</div>
  <div key="3">3</div>
  <div key="4">6</div>
</div>

`// 新
<div>
  <div key="4">4</div>
</div>`
  • 检测到新节点是object所以进入单节点对比流程
  • 从旧的节点中遍历查找是否可以复用的节点
  • 如果通过key在旧节点中查询到了,key相同,type不同 表示就没有可以复用的,那么兄弟节点都可以删除了
  • 旧节点遍历第一个key不相同,那么标记删除,继续遍历下一个节点,key:2,3都不相同同理都标记删除,直到找到key为4,type和key都相同可以复用。如果把新节点的类型改了比如新的节点改为下面的:
// 新
<div>
  <p key="4">4</p>
</div>
  • 依然通过key查找旧节点遍历第一个key不相同,那么当前节点删除。然后还是继续遍历下一个兄弟节点,因为同级的节点可能有可以复用的。重复这个对比过程
  • 因此在做react开发的过程中,如果一个超长列表更新过后变成只有一个节点的列表,也需要进行一次长列表的遍历

列表对比,多节点对比(新节点是多个节点)

按照更新优先对比(react团队认为更新场景会比其他场景更加频繁所以更新操作会被优先判断),并且多节点对比不止遍历一次。

  1. 从新节点的第一个节点开始和旧的fiber对比,判断是否可以复用
  2. 如果可以复用,继续按顺序取下一个新节点,并且旧节点也继续取fiber.sibling,如果可以复用就继续
  3. 如果不可以复用有下面的情况:

    1. key不同不可以复用,跳出遍历。
    2. key相同类型不同,标记旧的节点为删除,然后继续遍历
  4. 不管是新的节点列表遍历完,还是旧的节点遍历完了都停止遍历

所以终止遍历的情况如下:

1. key不相同
2. 老节点遍历完了
3. 新节点遍历完了

跳出后会存在的问题,但是新旧节点肯定会存在列表长度不一致的情况,那么:

  1. 理想情况下,两个列表都遍历完成
  2. 可能存在新节点未遍历完或者旧节点未遍历完
  3. 可能存在新旧列表都还未遍历完

因此对于上面的两种还有预留未处理的,还存在另外一次的遍历

第二次遍历:

  1. 理想情况新旧节点都遍历完了diff就结束了
  2. 新节点未遍历完,旧节点已经遍历完了,那就证明有插入操作,那就遍历剩余的新节点,生成新节点的插入位置
  3. 新节点遍历完了,旧节点还未遍历完,继续遍历剩余的旧的fiber,标记剩余fiber为删除
  4. 新旧节点都有剩余(证明可能有节点需要移动,发生位置变化)
  5. 如果有节点移动处理完移动之后还需要遍历一次老的节点列表,如果老的节点列表还有节点就要标记删除

对于节点的移动也就是老节点的复用需要单独说明一下:

react移动只对比两个位置,一个是节点在旧的列表中的位置,我们命名为oldIndex,一个是在对比过程中访问过的最大的旧的节点位置,我们命名为maxVisitedIndex,那么可以遇见:
  • oldIndex > maxVisitedIndex 不移动,并且更改maxVisitedIndex为oldIndex(maxVisitedIndex初始为0 遇见的旧位置oldIndex比之前遇见的最大旧位置maxIndex大,说明最大旧位置变了)
  • oldIndex === maxVisitedIndex,不移动
  • oldIndex < maxVisitedIndex, 就要移动到当前index位置。

移动节点举例:

// 旧
<div>
  <div key="1">1</div>
  <div key="2">2</div>
  <div key="3">3</div>
  <div key="4">4</div>
</div>

// 新
<div>
  <div key="2">2</div>
  <div key="1">1</div>
  <div key="4">4</div>
  <div key="3">3</div>
</div>

diff过程,移动的节点当然就是可以复用的节点:

  • index=0,新节点<div key="2">2</div>,maxVisitedIndex=0, oldIndex=1.根据交换原则,oldIndex > maxVisitedIndex,不移动。但是oldIndex比上次遇见的最大的旧的节点位置大,所以需要更新maxVisitedIndex=oldIndex,此时maxVisitedIndex=1;
  • index=1,新节点<div key="1">1</div>,maxVisitedIndex=1, oldIndex=0,根据交换原则,oldIndex < maxVisitedIndex,需要更新位置,则新节点<div key="1">1</div>移动位置到index的位置也就是移动到位置1.此时访问到的最大旧节点位置maxVisitedIndex依然是1没有变。
  • index=2,新节点<div key="4">4</div>,maxVisitedIndex==1, oldIndex=3,根据交换原则oldIndex > maxVisitedIndex,不移动。但是访问过的最大旧节点位置发生变化,所以更新maxVisitedIndex,maxVisitedIndex=oldIndex。此时maxVisitedIndex=3.
  • index=3,新节点<div key="3">3</div>,maxVisitedIndex=3,oldIndex=2,根据交换原则oldIndex < maxVisitedIndex,需要移动位置将新节点新节点<div key="3">3</div>移动到位置index,也就是移动到位置2去。

H_H_code
51 声望3 粉丝