React diff源码解析

497 阅读6分钟

DIFF核心函数 -- reconcileChildFibers

目的:生成新fiber diff源于脏检查 diff对象: workInProgressoldChildrenFibernewChildren (jsx) 进行对比。 优化: Fragment 若无key,则直接取 children 进行创建,不创建 FragmentFiber 无法复用:1. key不同 2.ket同type不同

newChildren 不同情况,进行不同 diff 。这里只分析3种情况

单JSX (newChildren值为单个jsx) -- reconcileSingleElement

  • **循环 **oldChildrenFiberchildFibernewChild(jsx) 进行对比
    • key 相同
      • newChildFragmentoldFiber.tag 也是 Fragment
        • 标记删除剩余oldChildrenFiber -- deleteChild()
        • existing  = 获取**复用fiber **-- useFiber()
          • 更新index,ref,return等属性。(会复用dom)
        • return existing
      • type 相同
        • 标记删除剩余oldChildrenFiber -- deleteChild()
        • existing  = 获取复用fiber -- useFiber()
          • 更新index,ref,return等属性。(会复用dom)
        • return existing
      • 走到此,说明 key 对应的组件无法复用
      • 标记删除剩余oldChildrenFiber -- deleteChild()
    • key 不同
      • 标记删除 childFiber  -- deleteChild()
    • child = child.sibling
  • 走到这,说明无可复用fiber
  • created = 创建 新fiber-- createFiberFromElement()
    • FragmentcreateFiberFromFragment()
  • return created
function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    const key = element.key;
    let child = currentFirstChild;
    while (child !== null) { // fiber update
      if (child.key === key) { // key相同
        const elementType = element.type;
        // 复用Fragment
        if (elementType === REACT_FRAGMENT_TYPE) {
          if (child.tag === Fragment) {
            // 删除
            deleteRemainingChildren(returnFiber, child.sibling);
            // 复用fiber
            const existing = useFiber(child, element.props.children);
            existing.return = returnFiber;
            return existing;
          }
        } else {
          // 复用其余type相同组件,以及复用lazy
          if ( 
            child.elementType === elementType ||
            (enableLazyElements &&
              typeof elementType === 'object' &&
              elementType !== null &&
              elementType.$$typeof === REACT_LAZY_TYPE &&
              resolveLazy(elementType) === child.type)
          ) {
            deleteRemainingChildren(returnFiber, child.sibling);
            const existing = useFiber(child, element.props);
            existing.ref = coerceRef(returnFiber, child, element);
            existing.return = returnFiber;
            return existing;
          }
        }
        // 删除所有,因为key匹配的无法复用,剩余都是key不匹配的也没法用
        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        //删除
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }
		// 创建 Fragment fiber
    if (element.type === REACT_FRAGMENT_TYPE) {
      const created = createFiberFromFragment(
        element.props.children,
        returnFiber.mode,
        lanes,
        element.key,
      );
      created.return = returnFiber;
      return created;
    } else {
		// 创建 ReactElement fiber
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      created.return = returnFiber;
      return created;
    }
  }

文本 (newChildren值为string或number时) -- reconcileSingleTextNode

注意:react对于原生组件 chilren 是单文本节点的情况进行优化,不会对其生成fiber

  • 不生成fibet => updateHostComponent中将newChildren设置为null
  • 节点插入文本 => completeWork-> case HostComponent -> finalizeInitialChildren -> setInitialProperties -> setInitialDOMProperties -> typeof nextProp === 'string'或'number' -> setTextContent

实现:

  • firstChildFiber 是否为文本节点
    • 标记删除其余 siblingFiber -- deleteChild()
      • existing  = 获取复用fiber -- useFiber()
        • 更新index,return等属性。(会复用dom)
    • return existing
  • 走到这里,说明无法复用。
  • 标记删除 childrenFiber -- deleteChild()
  • created = 创建 新fiber-- createFiberFromText()
  • return created
function reconcileSingleTextNode(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  textContent: string,
  lanes: Lanes,
): Fiber {
  // 复用
  if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
    deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
    const existing = useFiber(currentFirstChild, textContent);
    existing.return = returnFiber;
    return existing;
  }
  // 删除 新建
  deleteRemainingChildren(returnFiber, currentFirstChild);
  const created = createFiberFromText(textContent, returnFiber.mode, lanes);
  created.return = returnFiber;
  return created;
}

数组 (newChildren值为array,数组项为对应jsx) -- reconcileChildrenArray

  • 注意:
    • O(n)复杂度; oldChildrenFibernewChildrenJSX 都只遍历一次,遍历就会被处理。
    • 节点变化处理优先级:更新 > 删除 > 新增 > 位置变化
  • 模块变量:
    • resultingFirstChild : 最终返回的 fiber
    • previousNewFiber : 最新创建的 newFiber
    • lastPlacedIndex : 最后一个安置(Placement)的 fiber
    • oldFiber :表示正在遍历的 fiber , 从 currentFirstChild 遍历 siblingnull 。 
    • newChild :表示正在遍历的 jsx 。从newChildrenJSXnewIdx 确定。
    • newIdx : 表示正在遍历的 newChild 的索引。 
    1. 处理更新
    • 什么是更新?key不变,即算更新。 更新时尽量复用fiber,dom
      • 不写key,默认为null,新旧key都为null,也是key不变。
    • 循环 oldChildrenFiber** newChildrenJSX **。 childFibernewChild(jsx) 进行对比
      • 生成 newFiber -- updateSlot()
      • newFibernull
        • 说明此次不是更新。即key不同。
        • break
      • newFiber **没有复用 **oldFiber ,标记删除 oldFiber 。-- deleteChild()
        • 判断 oldFiber && newFiber.alternate === null
      • 安置 newFiber   
        • lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      • 判断 newFiber 是否为第一个子节点
          • resultingFirstChild 赋值为 newFiber
          • newFiber 通过 sibling 连接前一个子节点。reviousNewFiber.sibling = newFiber
      • previousNewFiber = newFiber
      • oldFiber = nextOldFiber
    1. 处理删除
    • 进入条件 newIdx === newChildren.length** **。
      • newChildren 已全被遍历,未被遍历的 oldChildrenFiber 不再需要,全部删除。
    • 标记删除剩余 oldChildrenFiber -- deleteChild()
    • return  resultingFirstChild
    1. 处理**新增 **
    • 进入条件 oldFiber === null** **。
      • oldChildrenFiber 已全被遍历,未被遍历的 newChildren 全部为新创建的 fiber
    • **循环 newChildrenJSX **。
      • 生成 newFiber
      • 放置 newFiber 。-- placeFiber()
      • 判断 newFiber 是否为第一个子节点
          • resultingFirstChild 赋值为 newFiber
          • newFiber 通过 sibling 连接前一个子节点。reviousNewFiber.sibling = newFiber
      • previousNewFiber = newFiber
    • return  resultingFirstChild
    1. 处理位置变化
    • 进入条件 oldChildrenFibernewChildren 都没遍历完。
    • 剩余oldChildrenfiber 生成 Map(key/index, childFiber) -- mapRemainingChildren()
      • Map的key优先取值fiber.key,若为null,取fiber.index。value值为fiber。
    • **循环 newChildrenJSX **。
      • 生成 newFiber -- updateFromMap()
        • 通过 key/indexMap 中取 oldFiber
        • 有则复用,无则新建。
      • 若为 复用fiber ,删除Map对应索引。
      • 安置 newFiber
        • lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx)
      • 判断 newFiber 是否为第一个子节点
          • resultingFirstChild 赋值为 newFiber
          • newFiber 通过 sibling 连接前一个子节点。reviousNewFiber.sibling = newFiber
      • previousNewFiber = newFiber
    1. 删除Map中未复用用fiber 。-- deleteChild()
    1. return  resultingFirstChild
function reconcileChildrenArray(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChildren: Array<*>,
  lanes: Lanes,
): Fiber | null {

  let resultingFirstChild: Fiber | null = null;
  let previousNewFiber: Fiber | null = null;
  let oldFiber = currentFirstChild;
  let lastPlacedIndex = 0;// 处理节点位置变化
  let newIdx = 0;
  let nextOldFiber = null;
  // 循环功能:  只处理更新
  // 循环终止条件: 1. oldChildren / newChildren 遍历完了
  //             2. oldFiber与newElement(jsx) key不同(位置交换), 在当前项 break
  // 更新是什么意思?  key不变,即算更新.   节点类型,内容,props
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    // index是在jsx中的位置,位置变了 跳出
    if (oldFiber.index > newIdx) {
      nextOldFiber = oldFiber;
      oldFiber = null;
    } else {
      nextOldFiber = oldFiber.sibling;
    }
    // 尝试update fiber 
    const newFiber = updateSlot(
      returnFiber,
      oldFiber,
      newChildren[newIdx],
      lanes,
    );
    // 能否更新?
    if (newFiber === null) {
      if (oldFiber === null) {
        oldFiber = nextOldFiber;
      }
      break;
    }
    if (shouldTrackSideEffects) {
      if (oldFiber && newFiber.alternate === null) {
        // 不复用 删除oldFiber
        deleteChild(returnFiber, oldFiber);
      }
    }
    // 将需要插入的fiber flags写入Placment
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    if (previousNewFiber === null) {
      // fiberChildren头
      resultingFirstChild = newFiber;
    } else {
      // 构建tree
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
  }

  if (newIdx === newChildren.length) {
    // 新的完成,删除剩余旧fiber
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }

  if (oldFiber === null) {
    // oldFiber遍历结完
    // 新的全部生成fiber
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
      if (newFiber === null) {
        continue;
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
    return resultingFirstChild;
  }
	// 生成Map(key/index,fiber)
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
	// 通过Map来复用
  for (; newIdx < newChildren.length; newIdx++) {
    // 复用 / 新生成
    const newFiber = updateFromMap(
      existingChildren,
      returnFiber,
      newIdx,
      newChildren[newIdx],
      lanes,
    );
    if (newFiber !== null) {
      if (shouldTrackSideEffects) {
        if (newFiber.alternate !== null) {
          // Map去除复用的fiber
          existingChildren.delete(
            newFiber.key === null ? newIdx : newFiber.key,
          );
        }
      }
      // 将需要插入的fiber flags写入Placment
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
  }

  if (shouldTrackSideEffects) {
    // Map 复用结束,删除剩余无法复用的fiber
    existingChildren.forEach(child => deleteChild(returnFiber, child));
  }

  return resultingFirstChild;
}

相关函数

deleteChild -- 标记删除childFiber

deleteChild(returnFiber: Fiber, childToDelete: Fiber)

  • 功能:
    • 收集要删除的 childFiber 。commit阶段进行删除。
  • 逻辑
    • returnFiber.flags 标记 ChildDeletion
    • returnFiber.deletions 数组push childFiber
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
  const deletions = returnFiber.deletions;
  if (deletions === null) {
    returnFiber.deletions = [childToDelete];
    returnFiber.flags |= ChildDeletion;
  } else {
    deletions.push(childToDelete);
  }
}

useFiber -- 复用fiber

useFiber(fiber: Fiber, pendingProps: mixed)

  • 功能:
    • 复用fiber。
  • 逻辑
    • clone = 生成新workInProgress,复用/新建fiber。
      • 复用:fiber有alternate
      • 新建:fiber没alternate
    • 重置props,index,sibling
      • clone.pendingProps = pendingProps
      • clone.index = 0;
      • clone.sibling = null
    • return clone
function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
  const clone = createWorkInProgress(fiber, pendingProps);
  clone.index = 0;
  clone.sibling = null;
  return clone;
}

placeChild -- 安置fiber

placeChild(newFiber: Fiber, lastPlacedIndex: number, newIndex: number )

  • 功能:
    • 判断 newFiber 是否需要安置 Palcement
  • 变量
    • lastPlacedIndex :最后一个**复用且不 ****Placement**fiber.index
      • 最后一个已经插入到 dom 且不用变化位置的 fiber.index ,即 复用fiber
  • 思路:
    • newFiber 分为 复用fiber ,和 新建fiber
    • React是如何插入DOM
      • insertOrAppendPlacementNode
      • fiber 右侧 sibling 中有 复用fiber ,则insertBefore,插入到它左侧。
      • fiber 右侧 sibling 中无 复用fiber ,则appenChild,依次插入。
    • 复用fiber  最后一个复用fiber 的左侧,则移动到右侧(安置)。
    • 复用fiber  不在 最后一个复用fiber 的左侧,则修改 lastPlacedIndexoldIndex
      • fiber.index 值为其在 childrenJSX数组index ,且 lastPlacedIndex 只会赋值 oldFiber.index
      • 所以 lastPlacedIndex 的值必定为childrenJSX数组 范围内的值。
        • lastPlacedIndex 初始值为 0 。因为数组起点是 0
    • 若为 新建fiber 直接安置。
  • 逻辑
    • 若为mount,直接return lastPlacedIndex
    • newFiber.index = newIndex; newFiberindexjsxnewChildrenJSX 的索引。
    • 判断是否为复用 fiber
        • 标记安置(Placement)
        • return lastPlacedIndex
      • 是·
        • 判断 复用fiber 是否在 最后一个复用fiber 的左侧。
          • oldFiber.index < lastPalcedIndex
        • 在左侧 <
          • 标记安置(Placement)
          • return lastPlacedIndex    
        • 不在左侧 >=, 修改lastPlacedIndex
          • return oldIndex。 会赋值给lastPlacedIndex    
function placeChild(
  newFiber: Fiber,
  lastPlacedIndex: number,
  newIndex: number,
): number {
  newFiber.index = newIndex;
  const current = newFiber.alternate;
  if (current !== null) {
    const oldIndex = current.index;
    // 若复用fiber 在 最后一个复用fiber 的 左侧 重新Placment
    if (oldIndex < lastPlacedIndex) {
      newFiber.flags |= Placement;
      return lastPlacedIndex;
    } else {
	    // 若复用fiber 不在左侧, 修改lastPlacedIndex
      return oldIndex;
    }
  } else {
    // 新fiber直接Placment
    newFiber.flags |= Placement;
    return lastPlacedIndex;
  }
}