加加 React 30 问 -- 2. Diff 算法是什么(3)-- 多节点 Diff ?

159 阅读3分钟

多节点 Diff

思路

  • 比对双方的 currentFiber 的 children ,是一个链表结构,另一方是 jsx 对象中的 children,是一个对象数组。
  • 每一次都只能从 jsx 中取一个 child 和当前的 fiber 做比对
  • 由于数据统计等原因, React 对更新处理的优先级高于新增/删除操作,所以在比对的时候,首先看能否复用更新,不行再做另外的操作
  • 整个 Diff 算法分两次遍历处理,第一次是为了遍历出可以更新的节点,第二次是为了处理其他不许与更新的节点。

第一次更新

第一次遍历的具体代码:

 /**
     * @分析 : 第一轮遍历
     * 1.遍历 newChildren, 用 newChildren[i] 与 oldFiber 进行比较,判断是否可以复用
     */
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
    //  1 VS 1 对比,生成 newFiber
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber === null) {
        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);
      if (previousNewFiber === null) {
        // TODO: Move out of the loop. This only happens for the first run.
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

遍历过程中,fiber 和单个节点的更新过程 -- 和单节点元素diff 很像

// 第一个遍历时,oldFiber 和 newChild 对比 ,只是 1 VS 1 的比较
// 有点类似单节点元素diff,但是这里不会随便就把 oldFiber 给干掉
function updateSlot(
    returnFiber,
    oldFiber,
    newChild,
    lanes
) {
    const key = oldFiber !== null ? oldFiber.key : null
    if (typeof newChild === 'string' || typeof newChild === 'number') {
        if (key !== null) {
            return null
        }
        return updateTextNode(returnFiber, oldFiber, "" + newChild, lanes)
    }

    if (typeof newChild === 'object' && newChild !== null) {
        switch (newChild.$$type) {
            case REACT_ELEMENT_TYPE:
                if (newChild.key === key) {
                    if (newChild.type === REACT_FRAGMENT_TYPE) {
                        xxx
                    }
                    return updateElement(returnFiber, oldFiber, newChild, lanes)
                } else {
                    return null
                }
            case REACT_PORTAL_TYPE: xxx
        }
    }

    // child 是数组或者类数组
    if (isArray(newChild) || getIteratorFn(newChild)) {
        if (key !== null) {
            return null
        }
        return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
    }
}

Diff 步骤

  1. 遍历 jsx 的对象数组,想 children[i] 和 oldFiber 比较,判断是否可以复用
  2. 如果可以,i++, oldFiber.sibling, 继续走下去
  3. 如果不可以复用
    • key 不同导致的不可复用,直接跳出第一次循环,也就是说更新查找结束
    • 如果是 type 类型的不同导致的不可复用,将 oldFiber 标记为 DELETE,继续遍历
  4. children 遍历完了,或者 oldFiber 走完了,退出循环

第一次循环结束会出现的结果

  • 步骤3跳出的,也就是没有走完就退出循环,children 和 oldFiber 都没有遍历完
  • 步骤4跳出的,可能是某一遍走完,也有可能是一起走完的

第二轮遍历

  • 因为我们是通过 children 和 oldFiber 比对的,但是最重要还是 children,所以主要分成三种情况
  • children 没有了,那么剩下的都是删除操作了
  • children 还有,oldFiber 没有了,那么剩下的都是新增操作
  • children 还有,oldFiber 也还有,那么需要第二次遍历来解决了

children没有了

 // jsx 对象数组已经结束 -- 证明剩下都是删除操作了
    if(newIdx === newChildren.length){
        // jsx 的对象数组遍历完了,那么就将剩下的 oldFiber 删除掉吧
        deleteRemainingChildren(returnFiber,oldFiber)
        return resultingFirstChild
    }

children 还有,但是 oldFiber 没有了

   // oldFiber 遍历完了,但是 newChildren 没有遍历完,说明有节点插入了
    if (oldFiber === null) {
        for (; newIdx < newChildren.length; newIdx++) {
            const newFiber = createChild(returnFiber, newChildren[newIdx], lanes)
            if (newFiber === null) {
                continue
            }
            lastPlaceIndex = placeChild(newFiber, lastPlaceIndex, newIdx)
            // 开始将 newFiber 链接到 WIPFiber 树中去了
            if (previousNewFiber === null) {
                // 只有在初始状态才会酱紫,更新阶段不会
                // 最后返回的就是 newFiber
                resultingFirstChild = newFiber
            } else {
                previousNewFiber.sibling = previousNewFiber
            }
            previousNewFiber = newFiber
        }
        return resultingFirstChild
    }

两者都还存在

  • 先把 fiber 中的值用 map 存起来
  • 遍历 children,然后根据再做对比,主要是找出可能是key交换的情况,也就是用 child 和整个 map 进行比对,看看是否能废物利用,直接替换,不要做删除新增操作
  • 每次比对后,看一下新的 fiber 是否有 sibling,有就是怎么是复用了的,那么就要从 map 中抽出来,以免最后用删除新增的方式进行更新
  • 遍历结束后,还存在于 map 中的节点就是 DELETE 掉的了
  • 最后返回 fiber
 // 两者都还存在 fiber
    // 将未处理的 fiber ,以 fiber.key 为 key,以 fiber 为 value 保存在一个 map 中
    const existingChildren = mapRemaingChildren(returnFiber, oldFiber)

    for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = updateFromMap(
            existingChildren,
            returnFiber,
            newIndex,
            newChildren[newIdx],
            lanes
        )

        if (newFiber !== null) {
            if (shouldTrackSideEffects) {
                if (newFiber.alternate !== null) {
                    // 存在 alternate,证明这个 newFiber 和 currentFiber 是一致的
                    // 所以应该直接在 existingChildren 中删除这个节点,防止现在 oldFiber 中删除了该节点,然后又重新造一个
                    existingChildren.delete(
                        newFiber.key === null ? newIdx : newFiber.key
                    )
                }
            }
            // 最后一个可复用的节点再 oldFiber 中的位置索引
            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;