React精简后的diff源码

117 阅读3分钟

fiber架构

未命名文件.png 我们先简单了解一下React的数据结构,通过jsx描述页面的数据结构,将jsx转化vdom,再转化成fiber。如上图是最简单的fiber树,父节点的child指向第一个子节点,子节点的sibling指向下一个兄弟节点,子节点上的return指向父节点。

多节点的diff

第一次遍历

遍历新节点的数组,复用key相同的老节点

    let newIdx = 0;
    let nextOldFiber = null;
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      nextOldFiber = oldFiber.sibling;
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber === null) {
        break;
      }
      oldFiber = nextOldFiber;
    }

我们可以看到第一次遍历结束有两个条件

  1. newChildren遍历完成
  2. 新老节点的key不相等,也就是newFibernull的时候。
    我们可以看下updateSlot函数,看下什么情况下返回null
 function updateSlot(
    returnFiber: Fiber,
    oldFiber: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    const key = oldFiber !== null ? oldFiber.key : null;
    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE: {
          if (newChild.key === key) {
           ...
          } else {
            return null;
          }
        }
        case REACT_PORTAL_TYPE: {
          if (newChild.key === key) {
            ...
          } else {
            return null;
          }
        }
      }

      if (isArray(newChild) || getIteratorFn(newChild)) {
        if (key !== null) {
          return null;
        }
      }
    }
    return null;
  }

当新老节点的key不相等的时候,结束第一次遍历

第二次遍历

如果旧节点遍历完成,将剩余新节点加上Placement副作用

 if (oldFiber === null) {
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
        ...
      }
    }

第三次遍历

如果新旧节点都没有遍历完成,先将剩余的oldFiber节点生成一个map,遍历新节点,在旧节点的map中获取可以复用的oldFiber生成新的newFiber,将newFiber通过placeChild函数移动到对应的位置

    // 初始化为0
    let lastPlacedIndex = 0;
    // 将剩余的oldFiber节点生成一个map
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    for (; newIdx < newChildren.length; newIdx++) {
      // 从旧节点的map中获取是否有可以用的fiber
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber !== null) {
        // 通过placeChild函数进行节点的移动
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        ...
      }
    }
    
  function placeChild(
    newFiber: Fiber,
    lastPlacedIndex: number,
    newIndex: number,
  ): number {
      const current = newFiber.alternate;
      const oldIndex = current.index;
      if (oldIndex < lastPlacedIndex) {
        // This is a move.
        newFiber.flags = Placement;
        return lastPlacedIndex;
      } else {
        // This item can stay in place.
        return oldIndex;
      }
  }

我们主要关心如何进行移动的,着重分析一下placeChild函数

  • 初始化lastPlacedIndex: let lastPlacedIndex = 0
  • oldIndex < lastPlacedIndex时,进行节点的移动
  • lastPlacedIndex的值是oldIndex和lastPlacedIndex两个中的最大值,即lastPlacedIndex = oldIndex < lastPlacedIndex ? lastPlacedIndex : oldIndex

未命名文件 (1).png 我们看下将ABCD,变成DABC的移动过程

  • 初始化lastPlacedIndex为0
  • 遍历newChildren, 找到D节点的oldIndex为3,oldIndex < lastPlacedIndex不成立,所以D节点不移动,将lastPlacedIndex更新为3
  • 找到A节点的ondIndex为0,oldIndex < lastPlacedIndex成立,将A移动到最后面
  • 找到B节点的ondIndex为1,oldIndex < lastPlacedIndex成立,将B移动到最后面
  • 找到C节点的ondIndex为2,oldIndex < lastPlacedIndex成立,将C移动到最后面

为什么不用Vue2的双端diff

在处理D节点插入到A节点前面这种情况,React的diff算法移动次数过多,为什么不采取Vue2的双端diff?
官方给出的解释:

  1. 反转列表和后面的节点插入到前面的场景比较少
  2. 双端diff需要向前查找节点,但是fiber节点没有反向指针,只能从前向后遍历