想想react会怎么做(9)之 diff核心逻辑

117 阅读4分钟

单节点 diff

单节点 diff 的核心逻辑在 reconcileSingleElement 方法中

reconcileSingleElement的返回值直接通过 placeSingleChild 包裹,而 placeSingleChild 只是会给 diff 时创建的新节点打上 Placement 标记(更新过程中)

 function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
    debugInfo: ReactDebugInfo | null,
  ): Fiber {
    // 当前element key
    const key = element.key;
    let child = currentFirstChild;
    // 初次渲染时child 为 null,更新时不为null,进入对比逻辑
    while (child !== null) {
      // 如果element key和原fiber key 相同的话考虑复用
      if (child.key === key) {
        const elementType = element.type;
        // 如果element类型为Fragment
        if (elementType === REACT_FRAGMENT_TYPE) {
          // 如果child的类型也为Fragment
          if (child.tag === Fragment) {
            // 因为已经匹配到了,就删除其他兄弟节点
            deleteRemainingChildren(returnFiber, child.sibling);
            // 使用 useFiber 复用当前fiber, props为element的children
            const existing = useFiber(child, element.props.children);
            existing.return = returnFiber;
            return existing;
          }
        } else {
          // 如果两者的type也相同
          if (child.elementType === elementType) { 
            // 因为已经匹配到了,就删除其他兄弟节点
            deleteRemainingChildren(returnFiber, child.sibling);
            // 使用 useFiber 复用当前fiber, props为element的children
            const existing = useFiber(child, element.props);
            // 将element的ref绑定到复用完成的fiber上
            coerceRef(returnFiber, child, existing, element);
            existing.return = returnFiber;
            return existing;
          }
        }
        // 复用完成之后原fiber剩余节点(因为已经有新的fiber了)
        deleteRemainingChildren(returnFiber, child);
        break;
      } 
      // 如果 key 不相同直接删除当前节点,后面的逻辑再去创建
      else {
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }

    // 后面都是创建逻辑
    if (element.type === REACT_FRAGMENT_TYPE) {
      // 创建Fragment
      const created = createFiberFromFragment(
        element.props.children,
        returnFiber.mode,
        lanes,
        element.key,
      );
      created.return = returnFiber;
      return created;
    } else {
      // 创建Fiber
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      coerceRef(returnFiber, currentFirstChild, created, element);
      created.return = returnFiber;
      return created;
    }
  }

多节点 diff

多节点 diff 核心的逻辑在reconcileChildrenArray方法中

diff的本质是右移策略:如果某个节点往前移动了,相当于他之前的节点都要往右(后)移

具体实现:通过lastPlacedIndex判断节点是不是应该右移,直接举例子:

function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<any>,
    lanes: Lanes,
    debugInfo: ReactDebugInfo | null,
  ): Fiber | null {
    // 第一个新节点
    let resultingFirstChild: Fiber | null = null;
    // 上一个新节点
    let previousNewFiber: Fiber | null = null;
    // 第一个老节点
    let oldFiber = currentFirstChild;
    // 右移标识索引
    let lastPlacedIndex = 0;
    // 遍历索引指针
    let newIdx = 0;
    // 下一个老节点
    let nextOldFiber = null;

    // 遍历新节点列表
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      // 获取下一个老节点
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      // 开始进行单节点diff
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
        debugInfo,
      );
      // 如果newFiber为null则是diff时发现key不同不能复用,直接跳出循环
      if (newFiber === null) {
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      if (shouldTrackSideEffects) {
        if (oldFiber && newFiber.alternate === null) {
          deleteChild(returnFiber, oldFiber);
        }
      }
      // 判断当前节点是否需要移动,需要则标记Placement,不需要则返回新的lastPlacedIndex
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      // 如果previousNewFiber为null,则当前newFiber是第一个,直接赋值给resultingFirstChild
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        // 如果previousNewFiber存证,则把当前newFiber拼接到previousNewFiber的sibling上
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    // 如果新节点列表遍历完了,就直接删除老节点列表剩下的fiber,然后返回resultingFirstChild
    if (newIdx === newChildren.length) {
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }

    // 如果当前老节点列表遍历完了,则新的列表不需要diff,直接创建
    if (oldFiber === null) {
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(
          returnFiber,
          newChildren[newIdx],
          lanes,
          debugInfo,
        );
        if (newFiber === null) {
          continue;
        }
        // 创建完之后也需要调用placeChild判断节点是否需要移动
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
      return resultingFirstChild;
    }

    // 逻辑走到这里,说明新老节点都存在,且不能复用

    // 使用Map存储当前的老节点列表,key为节点的key,value为节点
    const existingChildren = mapRemainingChildren(oldFiber);

    // 遍历新节点列表
    for (; newIdx < newChildren.length; newIdx++) {
      // 判断新节点是否存在于老节点的Map中,存在就进行复用
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
        debugInfo,
      );
      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.forEach(child => deleteChild(returnFiber, child));
    }

    return resultingFirstChild;
  }

图解分析

简单情况

老列表称为old(fibers),新列表称为new(children, reactElement)

image.png

从new的第一项c节点开始,lastPlacedIndex的初始值为0:

  1. c节点在old中的索引(oldIndex)为2,oldIndex(2) > lastPlacedIndex,所以不给c打标,并且lastPlacedIndex赋值为2
  2. f节点的oldIndex为4,oldIndex(4) > lastPlacedIndex(2),所以不给f打标,并且lastPlacedIndex赋值为4
  3. a节点的oldIndex为0,oldIndex(0) < lastPlacedIndex(4),所以给a打标(要右移),lastPlacedIndex不变
  4. e节点的oldIndex为3,oldIndex(3) < lastPlacedIndex(4),所以给e打标(要右移),lastPlacedIndex不变
  5. b节点的oldIndex为1,oldIndex(1) < lastPlacedIndex(4),所以给b打标(要右移),lastPlacedIndex不变

所以标记之后的结果为:

image.png

全部右移到最右边

特别一点的情况

image.png

  1. b节点在old中的索引(oldIndex)为1,oldIndex(1) > lastPlacedIndex(0),所以不给b打标,并且lastPlacedIndex赋值为1
  2. a节点的oldIndex为0,oldIndex(0) < lastPlacedIndex(1),所以给a打标
  3. f节点的oldIndex为4,oldIndex(4) > lastPlacedIndex(1),lastPlacedIndex赋值为4
  4. c节点的oldIndex为2,oldIndex(2) < lastPlacedIndex(4),所以给c打标
  5. e节点的oldIndex为3,oldIndex(3) < lastPlacedIndex(4),所以给e打标

所以打标结果为:

image.png

所以是右移到第一个没有打标的元素之前