React22-diff算法

607 阅读6分钟

1.时间复杂度

最好的算法都是o(n3) n为元素个数 如果每个元素都去diff的话复杂度很高 所以react做出啦三个限制

1.只对同级元素进行diff,不用对另一个树的所有元素进行diff

2.tag不同的两个元素会产生不同的树,div变为p,react会销毁div及其子孙节点,新建p

3.通过key这个prop来暗示哪些子元素在不同渲染下保持稳定

举例说明第3点

// 更新前
<div>
  <p >ka</p>
  <h3 >song</h3>
</div>

// 更新后
<div>
  <h3 >song</h3>
  <p>ka</p>
</div>
这种情况下diff react会把p删除然后新建h3 插入 然后删除h3创建p在传入
// 更新前
<div>
  <p key="ka">ka</p>
  <h3 key="song">song</h3>
</div>

// 更新后
<div>
  <h3 key="song">song</h3>
  <p key="ka">ka</p>
</div>
//有key的情况,p节点找到后面key相同的p节点所以节点可以复用 h3也可以复用 只需要做一个p append到h3后面的操作

2.单一节点的diff

1.diff 就是对比jsx和currentfiber的对象生成workingprogress的fiber

2.dom节点是否可复用?1.type必须相同 2.key也必须相同 满足这两个才能复用 先检测key是否同再检测type是否同,可复用就复用这个fiber 不过是换属性而已

image-20210521151158847

3.什么情况不能复用?

1.key不一样: 这函数一开始就判断key是否相同 不相同直接删除current的fiber,然后找兄弟节点去看是否key相同,为什么找兄弟节点呢 因为可能currentfiber有同级节点而jsx只是单个节点,还是会走到singleelement这个逻辑,所以要看currentfiber同级所有节点,不能旧删除是否可以复用。同时如果某个节点可以复用我们也需要将currentfiber的其他同级fiber删掉。都是为了下面这种情况。

如果都不一样则创建新的fiber

//current
<div></div>
<p></p>
//jsx
<p><p>

2.type不一样:直接把current的该fiber和兄弟fiber全部删除,因为能判断到type的时候key已经相同啦,其他兄弟节点的key不可能相同,所以直接全部不可以复用。之前key不同还要看兄弟key是否相同。

3.多节点diff

什么时候执行多节点diff?当jsx此次为数组即可,无论currentfiber是不是数组

一共有三种情况

1.节点更新

//情况1—— 节点属性变化
// 之前
<ul>
  <li key="0" className="before">0<li>
  <li key="1">1<li>
</ul>

// 之后  
<ul>
  <li key="0" className="after">0<li>
  <li key="1">1<li>
</ul>
//情况2—— 节点类型更新
// 之后  
<ul>
  <div key="0">0</div>
  <li key="1">1<li>
</ul>

2.节点新增或者减少

//情况1 —— 新增节点
// 之前
<ul>
  <li key="0">0<li>
  <li key="1">1<li>
</ul>

// 之后 
<ul>
  <li key="0">0<li>
  <li key="1">1<li>
  <li key="2">2<li>
</ul>
//情况2 —— 删除节点
// 之后 
<ul>
  <li key="1">1<li>
</ul>

3.节点位置变化

// 之前
<ul>
  <li key="0">0<li>
  <li key="1">1<li>
</ul>

// 之后
<ul>
  <li key="1">1<li>
  <li key="0">0<li>
</ul>

在我们做数组相关的算法题时,经常使用双指针从数组头和尾同时遍历以提高效率,但是这里却不行。

虽然本次更新的JSX对象 newChildren为数组形式,但是和newChildren中每个组件进行比较的是current fiber,同级的Fiber节点是由sibling指针链接形成的单链表,即不支持双指针遍历。

newChildren[0]fiber比较,newChildren[1]fiber.sibling比较。

所以无法使用双指针优化。

多节点diff 最终会产生一条fiber链表,不过最后返回的还是一个fiber(第一个fiber)作为child

基于以上原因,Diff算法的整体逻辑会经历两轮遍历:

第一轮遍历:处理更新的节点。 因为更新的概率是最大的

第二轮遍历:处理剩下的不属于更新的节点

第一轮遍历

  1. let i = 0,遍历newChildren,将newChildren[i]oldFiber比较,判断DOM节点是否可复用。
  2. 如果可复用,i++,继续比较newChildren[i]oldFiber.sibling,可以复用则继续遍历。
  3. 如果不可复用,分两种情况:
  • key不同导致不可复用,立即跳出整个遍历,第一轮遍历结束。(因为key不同是对应节点位置变化不属于更新节点,等到第二轮循环处理)
  • key相同type不同导致不可复用,会将oldFiber标记为DELETION(这样在commit阶段就会删除这个dom),并继续遍历
  1. 如果newChildren遍历完(即i === newChildren.length - 1)或者oldFiber遍历完(即oldFiber.sibling === null),跳出遍历,第一轮遍历结束。
function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
  ): Fiber | null {
    // This algorithm can't optimize by searching from both ends since we
    // don't have backpointers on fibers. I'm trying to see how far we can get
    // with that model. If it ends up not being worth the tradeoffs, we can
    // add it later.

    // Even with a two ended optimization, we'd want to optimize for the case
    // where there are few changes and brute force the comparison instead of
    // going for the Map. It'd like to explore hitting that path first in
    // forward-only mode and only go for the Map once we notice that we need
    // lots of look ahead. This doesn't handle reversal as well as two ended
    // search but that's unusual. Besides, for the two ended optimization to
    // work on Iterables, we'd need to copy the whole set.

    // In this first iteration, we'll just live with hitting the bad case
    // (adding everything to a Map) in for every insert/move.

    // If you change this code, also update reconcileChildrenIterator() which
    // uses the same algorithm.

    if (__DEV__) {
      // First, validate keys.
      let knownKeys = null;
      for (let i = 0; i < newChildren.length; i++) {
        const child = newChildren[i];
        knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
      }
    }

    let resultingFirstChild: Fiber | null = null;//!返回的fiber
    let previousNewFiber: Fiber | null = null;//!创建fiber链需要一个临时fiber来做连接

    let oldFiber = currentFirstChild;//!遍历到的current的fiber(旧的fiber)
    let lastPlacedIndex = 0;//!新创建的fiber节点对应的dom节点在页面中的位置 用来节点位置变化的
    let newIdx = 0;//!遍历到的jsx数组的索引
    let nextOldFiber = null;//!oldFiber的下一个fiber
    //!第一轮循环 处理节点更新的情况
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {//!fiber的index标记为当前fiber在同级fiber中的位置
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      const newFiber = updateSlot(//!判断节点是否可以复用 这个函数主要看key是否相同 不相同直接返回null 相同则继续判断type是否相同 不相同则创建新的fiber返回 把旧的fiber打上delete的tag 新的fiber打上place的tag type也相同则可以复用fiber返回
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber === null) {
        // TODO: This breaks on empty slots like null children. That's
        // unfortunate because it triggers the slow path all the time. We need
        // a better way to communicate whether this was a miss or null,
        // boolean, undefined, etc.
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      if (shouldTrackSideEffects) {
        if (oldFiber && newFiber.alternate === null) {
          // We matched the slot, but we didn't reuse the existing fiber, so we
          // need to delete the existing child.
          deleteChild(returnFiber, oldFiber);
        }
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//!给newfiber加上place的tag
      if (previousNewFiber === null) {
        // TODO: Move out of the loop. This only happens for the first run.
        resultingFirstChild = newFiber;
      } else {
        // TODO: Defer siblings if we're not at the right index for this slot.
        // I.e. if we had null values before, then we want to defer this
        // for each null value. However, we also don't want to call updateSlot
        // with the previous one.
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }
    //!新旧同时遍历完
    if (newIdx === newChildren.length) {
      // We've reached the end of the new children. We can delete the rest.
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }
    //!老的遍历完 新的没遍历完 遍历剩下的jsx的newchildren
    if (oldFiber === null) {
      // If we don't have any more existing children we can choose a fast path
      // since the rest will all be insertions.
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
        if (newFiber === null) {
          continue;
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//!直接把新的节点打上place 插入dom
        if (previousNewFiber === null) {
          // TODO: Move out of the loop. This only happens for the first run.
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
      return resultingFirstChild;
    }
    //!老的没遍历完 新的也没遍历完 证明key不同跳出啦  要处理节点位置变化的情况 我们要找到key相同的复用 那么为了在o(1)时间内找到 我们用map(key:oldfiber.key->value:oldfiber)数据结构
    // Add all children to a key map for quick lookups.
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    //!遍历剩下的newchldren
    // Keep scanning and use the map to restore deleted items as moves.
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = updateFromMap(//!找到newchildren的key对应的oldfiber 复用/新建fiber返回
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) {
            // The new fiber is a work in progress, but if there exists a
            // current, that means that we reused the fiber. We need to delete
            // it from the child list so that we don't add it to the deletion
            // list.
            existingChildren.delete(
              newFiber.key === null ? newIdx : newFiber.key,//!从map中去掉已经找到key的oldfiber
            );
          }
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//!新的fiber标记为插入 注意位置  (oldindex<lastplaceindex) 移动位置插入 因为老的fiber的index位置比新的页面位置小 肯定要移动插入了
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }

    if (shouldTrackSideEffects) {
      // Any existing children that weren't consumed above were deleted. We need
      // to add them to the deletion list.
      existingChildren.forEach(child => deleteChild(returnFiber, child));//!删除多余的oldfiber 因为新的children已经遍历完
    }

    return resultingFirstChild;
  }

ps:大部分来自卡颂源码讲解react.iamkasong.com/diff/multi.…