React Source Code

    5. React Fiber架构:React Fiber对象的创建、更新、提交

    Published
    November 20, 2022
    Reading Time
    6 min read
    Author
    Felix

    在上一篇文章中,我们详细讨论了Fiber的整体结构和链表。本文将深入探讨Fiber对象的创建过程、初始化提交以及Fiber树的更新机制。

    1. 创建Fiber对象

    Fiber对象的创建是React渲染过程的起点。当我们调用ReactDOM.createRoot().render()时,React开始创建Fiber树。

    1.1 创建RootFiber

    首先,React会创建一个RootFiber,它是整个Fiber树的根节点。

    function createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks) {
      const root = new FiberRootNode(containerInfo, tag, hydrate)
      const uninitializedFiber = createHostRootFiber(tag)
      root.current = uninitializedFiber
      uninitializedFiber.stateNode = root
      initializeUpdateQueue(uninitializedFiber)
      return root
    }
    
    function createHostRootFiber(tag) {
      let mode
      if (tag === ConcurrentRoot) {
        mode = ConcurrentMode | BlockingMode | StrictMode
      } else if (tag === BlockingRoot) {
        mode = BlockingMode | StrictMode
      } else {
        mode = NoMode
      }
      return createFiber(HostRoot, null, null, mode)
    }
    

    这个过程创建了FiberRoot和RootFiber。FiberRoot是整个应用的起点,而RootFiber则是组件树的根节点。

    1.2 创建子Fiber节点

    接下来,React会根据组件树递归地创建子Fiber节点。这个过程发生在reconcileChildren函数中:

    function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
      if (current === null) {
        // 如果是首次渲染,使用 mountChildFibers
        workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes)
      } else {
        // 如果是更新,使用 reconcileChildFibers
        workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes)
      }
    }
    

    mountChildFibersreconcileChildFibers都是ChildReconciler函数的返回值,它们的区别在于是否标记副作用。

    1.3 创建单个Fiber节点

    创建单个Fiber节点的核心逻辑在createFiberFromElement函数中:

    function createFiberFromElement(element, mode, lanes) {
      let owner = null
      const type = element.type
      const key = element.key
      const pendingProps = element.props
      const fiber = createFiberFromTypeAndProps(type, key, pendingProps, owner, mode, lanes)
      return fiber
    }
    
    function createFiberFromTypeAndProps(type, key, pendingProps, owner, mode, lanes) {
      let fiberTag = IndeterminateComponent
      // 根据组件类型确定 fiberTag
      if (typeof type === 'function') {
        if (shouldConstruct(type)) {
          fiberTag = ClassComponent
        }
      } else if (typeof type === 'string') {
        fiberTag = HostComponent
      }
    
      const fiber = createFiber(fiberTag, pendingProps, key, mode)
      fiber.elementType = type
      fiber.type = type
      fiber.lanes = lanes
    
      return fiber
    }
    

    这个过程根据React元素的类型、key和props创建对应的Fiber节点。

    2. 初始化提交

    创建完Fiber树后,React需要将这些虚拟的Fiber节点渲染到实际的DOM中。这个过程称为"提交"(commit)。

    2.1 提交前的准备

    在进入提交阶段之前,React会做一些准备工作:

    function commitRoot(root) {
      const renderPriorityLevel = getCurrentPriorityLevel()
      runWithPriority(ImmediatePriority, commitRootImpl.bind(null, root, renderPriorityLevel))
      return null
    }
    
    function commitRootImpl(root, renderPriorityLevel) {
      do {
        flushPassiveEffects()
      } while (rootWithPendingPassiveEffects !== null)
    
      flushRenderPhaseStrictModeWarningsInDEV()
    
      if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
        throw new Error('Should not already be working.')
      }
    
      const finishedWork = root.finishedWork
      const lanes = root.finishedLanes
    
      if (finishedWork === null) {
        return null
      }
      root.finishedWork = null
      root.finishedLanes = NoLanes
    
      if (finishedWork === root.current) {
        throw new Error(
          'Cannot commit the same tree as before. This error is likely caused by ' + 'a bug in React. Please file an issue.'
        )
      }
    
      // 提交阶段开始
      root.callbackNode = null
      root.callbackPriority = NoLane
    
      // 更新 remainingLanes 和 pendingLanes
      let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes)
      markRootFinished(root, remainingLanes)
    
      if (root === workInProgressRoot) {
        workInProgressRoot = null
        workInProgress = null
        workInProgressRootRenderLanes = NoLanes
      }
    
      // 获取副作用列表
      let firstEffect
      if (finishedWork.flags > PerformedWork) {
        if (finishedWork.lastEffect !== null) {
          finishedWork.lastEffect.nextEffect = finishedWork
          firstEffect = finishedWork.firstEffect
        } else {
          firstEffect = finishedWork
        }
      } else {
        firstEffect = finishedWork.firstEffect
      }
    
      // 开始提交三个子阶段
      // ...
    }
    

    这里,React将提交过程的优先级设置为最高(ImmediatePriority),确保提交过程不会被打断。同时,它还处理了一些准备工作,如刷新被动效果和获取副作用列表。

    而使用最高优先级执行提交过程,确保更新能够尽快应用到DOM,我理解其实有几个原因:

    1. 用户体验:提交阶段直接影响到用户可见的UI变化。通过给予最高优先级,React确保这些变化能够尽快呈现给用户,提高应用的响应性。
    2. 一致性保证:提交阶段需要同步执行以保证UI的一致性。如果这个过程被中断,可能会导致UI处于不一致的状态。

    2.2 提交阶段

    提交阶段分为三个子阶段:Before mutation、Mutation和Layout,简化后代码如下

    function commitRootImpl(root, renderPriorityLevel) {
      // ...
    
      // 第一阶段:Before mutation
      commitBeforeMutationEffects()
    
      // 第二阶段:Mutation
      commitMutationEffects(root, renderPriorityLevel)
    
      // 切换当前树
      root.current = finishedWork
    
      // 第三阶段:Layout
      commitLayoutEffects(root, lanes)
    
      // ...
    }
    

    每个阶段都有特定的任务:

    1. Before mutation阶段:
      • 处理DOM操作前的准备工作
      • 调度useEffect
    function commitBeforeMutationEffects() {
      while (nextEffect !== null) {
        const current = nextEffect.alternate
    
        if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
          // ...
        }
    
        const flags = nextEffect.flags
        if ((flags & Snapshot) !== NoFlags) {
          commitBeforeMutationEffectOnFiber(current, nextEffect)
        }
        if ((flags & Passive) !== NoFlags) {
          if (!rootDoesHavePassiveEffects) {
            rootDoesHavePassiveEffects = true
            scheduleCallback(NormalSchedulerPriority, () => {
              flushPassiveEffects()
              return null
            })
          }
        }
        nextEffect = nextEffect.nextEffect
      }
    }
    
    1. Mutation阶段:
      • 执行实际的DOM操作
      • 调用生命周期方法
      • 重置文本节点
    function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
      while (nextEffect !== null) {
        const flags = nextEffect.flags;
    
        if (flags & ContentReset) {
          commitResetTextContent(nextEffect);
        }
    
        if (flags & Ref) {
          const current = nextEffect.alternate;
          if (current !== null) {
            commitDetachRef(current);
          }
        }
    
        const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
        switch (primaryFlags) {
          case Placement: {
            commitPlacement(nextEffect);
            nextEffect.flags &= ~Placement;
            break;
          }
          case PlacementAndUpdate: {
            commitPlacement(nextEffect);
            nextEffect.flags &= ~Placement;
            const current = nextEffect.alternate;
            commitWork(current, nextEffect);
            break;
          }
          case Update: {
            const current = nextEffect.alternate;
            commitWork(current, nextEffect);
            break;
          }
          case Deletion: {
            commitDeletion(root, nextEffect, renderPriorityLevel);
            break;
          }
        }
    
        nextEffect = nextEffect.nextEffect;
      }
    }
    
    1. Layout阶段:
      • 处理DOM操作后的工作
      • 调用useLayoutEffect钩子
      • 更新ref
    function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
      while (nextEffect !== null) {
        const flags = nextEffect.flags;
    
        if (flags & (Update | Callback)) {
          const current = nextEffect.alternate;
          commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
        }
    
        if (flags & Ref) {
          commitAttachRef(nextEffect);
        }
    
        nextEffect = nextEffect.nextEffect;
      }
    }
    

    3. Fiber树的更新机制

    React的更新机制是其性能优化的核心。当组件状态发生变化时,React会创建一个新的Fiber树(称为workInProgress树),然后与当前的Fiber树进行对比,找出需要更新的部分。

    3.1 调度更新

    当我们调用setState或使用hooks更新状态时,React会调度一次更新,这部分源码其实在第3章讲得很多了:

    function scheduleUpdateOnFiber(fiber: Fiber, lane: Lane, eventTime: number) {
      const root = markUpdateLaneFromFiberToRoot(fiber, lane);
      if (root === null) {
        return null;
      }
    
      // 标记根节点为已调度
      markRootUpdated(root, lane, eventTime);
    
      if (root === workInProgressRoot) {
        // 如果我们正在处理这个根节点,可能需要调整优先级
        if (
          workInProgressRootExitStatus === RootSuspendedWithDelay ||
          (workInProgressRootExitStatus === RootSuspended &&
            includesOnlyRetries(workInProgressRootRenderLanes) &&
            now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
        ) {
          // 中断当前渲染
          prepareFreshStack(root, NoLanes);
        } else {
          // 继续当前渲染,合并新的更新
          workInProgressRootRenderLanes = mergeLanes(
            workInProgressRootRenderLanes,
            lane,
          );
        }
      }
    
      ensureRootIsScheduled(root, eventTime);
      if (
        lane === SyncLane &&
        executionContext === NoContext &&
        (fiber.mode & ConcurrentMode) === NoMode
      ) {
        // 同步更新,立即执行
        performSyncWorkOnRoot(root);
      }
    
      return root;
    }
    

    3.2 构建workInProgress树

    在React的更新过程中,这个过程的核心是createWorkInProgress函数,它负责创建或重用Fiber节点来构建新的树结构。我们前面提到过,但我们现在深入看看这个设计:

    function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
      let workInProgress = current.alternate;
    
      if (workInProgress === null) {
        // 如果alternate不存在,创建一个新的Fiber
        workInProgress = createFiber(
          current.tag,
          pendingProps,
          current.key,
          current.mode,
        );
        workInProgress.elementType = current.elementType;
        workInProgress.type = current.type;
        workInProgress.stateNode = current.stateNode;
    
        workInProgress.alternate = current;
        current.alternate = workInProgress;
      } else {
        // 如果alternate存在,重置workInProgress
        workInProgress.pendingProps = pendingProps;
        workInProgress.type = current.type;
        workInProgress.flags = NoFlags;
        workInProgress.nextEffect = null;
        workInProgress.firstEffect = null;
        workInProgress.lastEffect = null;
        // ... 其他属性的重置
      }
    
      // 复制一些不变的属性
      workInProgress.childLanes = current.childLanes;
      workInProgress.lanes = current.lanes;
      workInProgress.child = current.child;
      workInProgress.memoizedProps = current.memoizedProps;
      workInProgress.memoizedState = current.memoizedState;
      workInProgress.updateQueue = current.updateQueue;
    
      // 处理dependencies
      const currentDependencies = current.dependencies;
      workInProgress.dependencies =
        currentDependencies === null
          ? null
          : {
              lanes: currentDependencies.lanes,
              firstContext: currentDependencies.firstContext,
            };
    
      // 复制其他属性
      workInProgress.sibling = current.sibling;
      workInProgress.index = current.index;
      workInProgress.ref = current.ref;
    
      return workInProgress;
    }
    

    这个函数的主要目的是创建或重用一个Fiber节点,作为当前更新过程中的工作单元:

    1. Fiber节点复用

      • React使用alternate属性来实现Fiber节点的复用。每个Fiber节点都有一个alternate,指向其在另一棵树中的对应节点。
      • 如果alternate存在,React会重用这个节点,而不是创建新的节点。这大大减少了内存分配和垃圾回收的开销。
    2. 选择性重置

      • 当重用一个Fiber节点时,React只重置那些可能发生变化的属性(如pendingPropsflagseffects等)。
      • 不变的属性(如childLaneslaneschild等)直接从当前Fiber复制,避免不必要的操作。
    3. 浅拷贝

      • 对于大多数属性,React使用浅拷贝。这意味着引用类型的属性(如updateQueuememoizedState等)只复制引用,而不是深度克隆(react几乎都是浅拷贝)。
      • 这种策略在大多数情况下是高效的,但也要求开发者在操作这些属性时要小心,避免意外修改。

    3.3 Diff算法

    React的Diff算法是其高效更新的核心,用于比较两棵虚拟DOM树的差异,以最小化实际DOM操作。React的Diff算法基于三个主要假设:

    1. 不同类型的元素会产生不同的树结构。
    2. 开发者可以通过key属性来暗示哪些子元素在不同的渲染中可能是稳定的。
    3. 只对同级元素进行比较。

    3.3.1 Diff算法的入口

    Diff算法的入口是reconcileChildFibers函数,几乎所有的更新和渲染过程都会用到这个函数:

    function reconcileChildFibers(
      returnFiber: Fiber,
      currentFirstChild: Fiber | null,
      newChild: any,
      lanes: Lanes
    ): Fiber | null {
      // 处理单个子元素
      if (typeof newChild === 'object' && newChild !== null) {
        switch (newChild.$$typeof) {
          case REACT_ELEMENT_TYPE:
            return placeSingleChild(
              reconcileSingleElement(
                returnFiber,
                currentFirstChild,
                newChild,
                lanes
              )
            );
          // ... 其他类型的处理
        }
      }
    
      // 处理多个子元素
      if (isArray(newChild)) {
        return reconcileChildrenArray(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes
        );
      }
    
      // 处理文本节点
      if (typeof newChild === 'string' || typeof newChild === 'number') {
        return placeSingleChild(
          reconcileSingleTextNode(
            returnFiber,
            currentFirstChild,
            '' + newChild,
            lanes
          )
        );
      }
    
      // 其他情况,删除所有现有子节点
      return deleteRemainingChildren(returnFiber, currentFirstChild);
    }
    

    3.3.2 单节点对比

    对于单个子元素,React使用reconcileSingleElement函数进行对比:

    function reconcileSingleElement(
      returnFiber: Fiber,
      currentFirstChild: Fiber | null,
      element: ReactElement,
      lanes: Lanes
    ): Fiber {
      const key = element.key;
      let child = currentFirstChild;
    
      while (child !== null) {
        // 比较key
        if (child.key === key) {
          // 比较type
          if (child.elementType === element.type) {
            deleteRemainingChildren(returnFiber, child.sibling);
            const existing = useFiber(child, element.props);
            existing.ref = coerceRef(returnFiber, child, element);
            existing.return = returnFiber;
            return existing;
          }
          // key相同但type不同,删除所有旧的子节点
          deleteRemainingChildren(returnFiber, child);
          break;
        } else {
          deleteChild(returnFiber, child);
        }
        child = child.sibling;
      }
    
      // 创建新的Fiber节点
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      created.return = returnFiber;
      return created;
    }
    

    单节点对比的过程主要包括:

    1. 比较key和type
    2. 复用或创建新节点
    3. 删除不需要的旧节点

    3.3.3 多节点对比

    对于多个子元素,React使用reconcileChildrenArray函数进行对比:

    function reconcileChildrenArray(
      returnFiber: Fiber,
      currentFirstChild: Fiber | null,
      newChildren: Array<any>,
      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;
    
      // 第一次遍历:处理更新的节点
      for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
        if (oldFiber.index > newIdx) {
          nextOldFiber = oldFiber;
          oldFiber = null;
        } else {
          nextOldFiber = oldFiber.sibling;
        }
        const newFiber = updateSlot(
          returnFiber,
          oldFiber,
          newChildren[newIdx],
          lanes
        );
        if (newFiber === null) {
          if (oldFiber === null) {
            oldFiber = nextOldFiber;
          }
          break;
        }
        if (shouldTrackSideEffects) {
          if (oldFiber && newFiber.alternate === null) {
            deleteChild(returnFiber, oldFiber);
          }
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
        oldFiber = nextOldFiber;
      }
    
      // 所有新子节点都已处理完毕
      if (newIdx === newChildren.length) {
        deleteRemainingChildren(returnFiber, oldFiber);
        return resultingFirstChild;
      }
    
      // 所有旧子节点都已处理完毕,添加剩余的新节点
      if (oldFiber === null) {
        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中,用于后续的查找
      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) {
              existingChildren.delete(
                newFiber.key === null ? newIdx : newFiber.key
              );
            }
          }
          lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
          if (previousNewFiber === null) {
            resultingFirstChild = newFiber;
          } else {
            previousNewFiber.sibling = newFiber;
          }
          previousNewFiber = newFiber;
        }
      }
    
      // 删除Map中剩余的旧节点
      if (shouldTrackSideEffects) {
        existingChildren.forEach(child => deleteChild(returnFiber, child));
      }
    
      return resultingFirstChild;
    }
    

    多节点对比的过程主要包括:

    1. 第一次遍历:尝试更新现有节点
    2. 处理新增节点
    3. 处理需要移动的节点
    4. 删除不再需要的旧节点

    3.3.4 Diff算法的优化策略

    1. 两次遍历:第一次处理可以直接更新的节点,第二次处理需要移动或新建的节点。
    2. 使用key进行优化:通过key可以快速判断元素是否可以复用。
    3. 从两端向中间比较:这种策略可以快速处理头尾的增删操作。
    4. 使用Map存储剩余节点:提高查找效率。
    5. 就地复用:尽可能地复用已有的Fiber节点。
    6. 批量处理:将多个更新操作批量处理,减少渲染次数。

    通过这些优化策略,React的Diff算法能够在O(n)的时间复杂度内完成对比,大大提高了性能。理解Diff算法的工作原理对于优化React应用非常重要,例如:

    1. 合理使用key属性,特别是在列表渲染中,避免使用索引作为key。
    2. 尽量保持组件的稳定性,避免不必要的重新渲染。
    3. 合理拆分组件,避免大型组件导致的大范围Diff。

    3.4 完成更新

    一旦Diff完成,React会标记需要更新的Fiber节点,然后在commit阶段应用这些更新。这个过程与初始渲染时的commit阶段类似,但只会处理被标记为需要更新的节点。

    总结

    React Fiber架构通过可中断的渲染过程和精细的更新控制,大大提高了React应用的性能和响应性。通过创建、更新和提交Fiber对象,React能够灵活地管理组件树,实现高效的状态更新和DOM操作。理解这些内部机制对于深入掌握React和优化React应用至关重要。

    在实际开发中,我们可以利用这些知识来优化我们的React应用:

    1. 合理使用key属性,帮助React更好地识别列表中的元素变化。
    2. 使用React.memouseMemouseCallback来避免不必要的重渲染。
    3. 理解并正确使用生命周期方法和Hooks,避免在不恰当的时机进行昂贵的操作。
    4. 对于大型列表,考虑使用虚拟化技术(如react-window)来减少渲染的节点数量。

    通过深入理解React Fiber的工作原理,我们可以编写出更高效、更可维护的React应用。