Oasis's Cloud

一个人的首要责任,就是要有雄心。雄心是一种高尚的激情,它可以采取多种合理的形式。
—— 《一个数学家的辩白》

React 基础渲染流程

基于 v19.2.0 版本

作者:oasis


本文通过下述 Demo 分析 React v19.2.0 的基础渲染流程。

import { createRoot } from 'react-dom/client'
function App() {
  return (
    <div>
      <h1>基础渲染流程</h1>
      <p>观察 createRoot 和首次渲染的执行过程</p>
    </div>
  )
}
// 断点 1: createRoot 调用前
debugger;
const root = createRoot(document.getElementById('root'));
// 断点 2: render 调用前
debugger;
root.render(<App />);

export default App

创建阶段

从 Demo 中可以看出,初始化流程主要分为两个步骤: 1. createRoot() 2. render(<App />)

本文会按照 React 代码的执行顺序进行分析。在稍后会给出适当的更高层级的抽象说明。本着自底向上的方式逐步分析理解 React 的渲染流程。

createRoot() 创建 FiberRootNode 和 ReactDomRoot 对象实例

createRoot 方法关键逻辑如下:

// packages/react-dom/src/client/ReactDOMRoot.js
export function createRoot(
    container: Element | Document | DocumentFragment,
    options?: CreateRootOptions,
): RootType {
    if (!isValidContainer(container)) {
        throw new Error('Target container is not a DOM element.');
    }

    warnIfReactDOMContainerInDEV(container);

    const concurrentUpdatesByDefaultOverride = false;
    let isStrictMode = false;
    let identifierPrefix = '';
    let onUncaughtError = defaultOnUncaughtError;
    let onCaughtError = defaultOnCaughtError;
    let onRecoverableError = defaultOnRecoverableError;
    let onDefaultTransitionIndicator = defaultOnDefaultTransitionIndicator;
    let transitionCallbacks = null;

    if (options !== null && options !== undefined) {
        if (options.unstable_strictMode === true) {
            isStrictMode = true;
        }
        if (options.identifierPrefix !== undefined) {
            identifierPrefix = options.identifierPrefix;
        }
        if (options.onUncaughtError !== undefined) {
            onUncaughtError = options.onUncaughtError;
        }
        if (options.onCaughtError !== undefined) {
            onCaughtError = options.onCaughtError;
        }
        if (options.onRecoverableError !== undefined) {
            onRecoverableError = options.onRecoverableError;
        }
        if (enableDefaultTransitionIndicator) {
            if (options.onDefaultTransitionIndicator !== undefined) {
                onDefaultTransitionIndicator = options.onDefaultTransitionIndicator;
            }
        }
        if (options.unstable_transitionCallbacks !== undefined) {
            transitionCallbacks = options.unstable_transitionCallbacks;
        }
    }

    const root = createContainer(
        container,
        ConcurrentRoot,
        null,
        isStrictMode,
        concurrentUpdatesByDefaultOverride,
        identifierPrefix,
        onUncaughtError,
        onCaughtError,
        onRecoverableError,
        onDefaultTransitionIndicator,
        transitionCallbacks,
    );
    markContainerAsRoot(root.current, container);

    const rootContainerElement: Document | Element | DocumentFragment =
        !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE
            ? (container.parentNode: any)
: container;
    listenToAllSupportedEvents(rootContainerElement);

    // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
    return new ReactDOMRoot(root);
}

在程序执行时,container 参数的值是 DOM Element 对象即 document.getElementById('root') 获取到的 div#root

createRoot 方法内部调用 createFiberRoot(container),然后返回 new FiberRootNode() 创建的 FiberRoot 实例。之后通过 createHostRootFiber 创建 HostRootFiber,它是宿主树的根节点。

react-1-1

创建根 Fiber 后,通过 new ReactDOMRoot() 实例化一个对象,此对象有如下几个属性: 1. _internalRoot 属性的值是 FiberRootNode 2. render 方法 3. unmount 方法

其中 renderunmount 方法挂在 prototype 上面。

render() 开启 Fiber 构建之路

由于 root.render(<App />) 的执行,所以会先创建 App 的 ReactElement 节点。如下图:

React-1-2.png

ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
    function() {
        const root = this._internalRoot;
        updateContainer(children, root, null, null);
    }
// packages/react-reconciler/src/ReactFiberReconciler.js
function updateContainer(
    element: ReactNodeList,
    container: OpaqueRoot,
    parentComponent: ?component(...props: any),
    callback: ?Function,
): Lane {
    const current = container.current;
    const lane = requestUpdateLane(current);
    updateContainerImpl(
        current,
        lane,
        element,
        container,
        parentComponent,
        callback,
    );
    return lane;
}
function updateContainerImpl(
    rootFiber: Fiber,
    lane: Lane,
    element: ReactNodeList,
    container: OpaqueRoot,
    parentComponent: ?component(...props: any),
    callback: ?Function,
): void {
    const update = createUpdate(lane);
    update.payload = {element};
    // 这里会将 App ReactElement 插入队列.
    // 将 update 加入 `updateQueue.shared.pending`(循环链表)。
    const root = enqueueUpdate(rootFiber, update, lane);
    if (root !== null) {
        startUpdateTimerByLane(lane, 'root.render()', null);
        scheduleUpdateOnFiber(root, rootFiber, lane);
        entangleTransitions(root, rootFiber, lane);
    }
}

render 方法内部调用 scheduleUpdateOnFiber(root, rootFiber, lane)entangleTransitions(root, rootFiber, lane)

调度阶段

scheduleUpdateOnFiber

scheduleUpdateOnFiber 是 React 调度更新的核心入口。当组件状态更新(如 setStateforceUpdate)时,该函数负责:

  1. 标记根节点有待处理的更新
  2. 根据更新优先级安排调度
  3. 处理渲染阶段更新和并发更新的边界情况

entangleTransitions

entangleTransitions 用于“纠缠”(entangle)多个 Transition 更新,确保它们在同一批次中一起处理,避免单独渲染导致不一致状态。

假设在一个 startTransition 中有多个 setState

startTransition(()=>{
  setState1();
  setState2();
  setState3();
});
entangleTransitions 会:

  1. lane A、B、C 合并到 sharedQueue.lanes
  2. 标记根节点,使 A、B、C 必须一起处理
  3. 确保不会单独渲染其中某个更新

scheduleUpdateOnFiber 方法中会调用 ensureRootIsScheduled(root)

根据 ensureRootIsScheduled 中的注释可以知道:ensureRootIsScheduled 确保根节点(Root)被加入调度队列,并确保有微任务来处理这个队列。当根节点收到更新时调用。

ensureRootIsScheduled 调用 scheduleImmediateRootScheduleTaskscheduleImmediateRootScheduleTask 用于在当前事件循环结束时,通过微任务(microtask)或立即任务处理所有待调度的根节点(Root),确保 React 更新在事件处理完成后统一调度。

// ReactFiberRootScheduler.js
function scheduleImmediateRootScheduleTask() {
  if (supportsMicrotasks) {
    scheduleMicrotask(() => {
      processRootScheduleInMicrotask();
    });
  }
}

scheduleImmediateRootScheduleTask 中会调用 processRootScheduleInMicrotask

function processRootScheduleInMicrotask() {
    const currentTime = now();

    let prev = null;
    let root = firstScheduledRoot;
    while (root !== null) {
        const next = root.next;
        const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
        if (nextLanes === NoLane) {
            // This root has no more pending work. Remove it from the schedule. To
            // guard against subtle reentrancy bugs, this microtask is the only place
            // we do this — you can add roots to the schedule whenever, but you can
            // only remove them here.

            // Null this out so we know it's been removed from the schedule.
            root.next = null;
            if (prev === null) {
                // This is the new head of the list
                firstScheduledRoot = next;
            } else {
                prev.next = next;
            }
            if (next === null) {
                // This is the new tail of the list
                lastScheduledRoot = prev;
            }
        } else {
            // This root still has work. Keep it in the list.
            prev = root;

            // This is a fast-path optimization to early exit from
            // flushSyncWorkOnAllRoots if we can be certain that there is no remaining
            // synchronous work to perform. Set this to true if there might be sync
            // work left.
            if (
                // Skip the optimization if syncTransitionLanes is set
                syncTransitionLanes !== NoLanes ||
                // Common case: we're not treating any extra lanes as synchronous, so we
                // can just check if the next lanes are sync.
                includesSyncLane(nextLanes) ||
                (enableGestureTransition && isGestureRender(nextLanes))
            ) {
                mightHavePendingSyncWork = true;
            }
        }
        root = next;
    }
}

processRootScheduleInMicrotask 方法中的关键步骤如下:

  1. 遍历调度队列
    • firstScheduledRoot 开始遍历所有根节点
    • 对每个根节点调用 scheduleTaskForRootDuringMicrotask
  2. 移除没有工作的根节点
    • 如果 nextLanes === NoLane,说明没有待处理工作
    • 从链表中移除该根节点
    • 这是唯一可以移除根节点的地方(防止重入问题)
  3. 更新同步工作标志
    • 如果根节点有同步工作,设置 mightHavePendingSyncWork = true
    • 用于优化 flushSync 的性能

processRootScheduleInMicrotask 内会调用 scheduleTaskForRootDuringMicrotask

function scheduleTaskForRootDuringMicrotask(
  root: FiberRoot,
  currentTime: number,
): Lane {
    // 为根节点创建一个 Scheduler 任务,用于异步执行 React 的渲染工作。它是并发渲染的关键调度点。
    const newCallbackNode = scheduleCallback(
        schedulerPriorityLevel,
        performWorkOnRootViaSchedulerTask.bind(null, root),
    );
}
scheduleTaskForRootDuringMicrotask 的作用:

  1. 任务调度:为根节点安排渲染任务
  2. 优先级管理:确定下一个要处理的 lanes 并映射到调度器优先级
  3. 过期处理:标记饥饿的 lanes 为过期
  4. 同步/异步区分:同步工作直接刷新,异步工作通过调度器
  5. 任务复用:优先级未变时复用现有任务
  6. 挂起处理:正确处理 Suspended 状态

这是 React 调度系统的核心函数,连接了 React 的优先级系统和浏览器的任务调度机制。

渲染阶段

performWorkOnRootViaSchedulerTask 内部调用 performWorkOnRoot(root, lanes, forceSync) 进入工作循环。在进入工作循环之前,需要先设置 workInProgress

function renderRootSync(
  root: FiberRoot,
  lanes: Lanes,
  shouldYieldForPrerendering: boolean,
): RootExitStatus {
    if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
        prepareFreshStack(root, lanes);
    }
    workLoopSync();
}   

prepareFreshStack(root, lanes) 中会通过 createWorkInProgress(current, pendingProps) 设置全局变量 workInProgress。此时的 workInProgress 指向根据 HostRootFiber 创建的 Fiber 节点。

prepareFreshStack 方法中调用 finishQueueingConcurrentUpdates()updateContainerImpl() 期间暂存的并发更新添加到对应的 fiber/hook 队列中。

设置 WIP,开始构建 Fiber 树

beginWork() 会根据 Fiber 节点的 tag 属性进行对应的处理。从 React 源码中可以找到不同的 tag 类型:

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;
export const HostHoistable = 26;
export const HostSingleton = 27;
export const IncompleteFunctionComponent = 28;
export const Throw = 29;
export const ViewTransitionComponent = 30;
export const ActivityComponent = 31;

workInProgress 上的 tag 属性为 3,说明是 HostRootbeginWork 会将 workInProgress 转到处理 HostRoot 的逻辑上。

HostRoot 的处理由下面函数完成:

function updateHostRoot(
  current: null | Fiber,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
   pushHostRootContext(workInProgress);

   const nextProps = workInProgress.pendingProps;
   const prevState: RootState = workInProgress.memoizedState;
   const prevChildren = prevState.element;
   cloneUpdateQueue(current, workInProgress);
   processUpdateQueue(workInProgress, nextProps, null, renderLanes);

   const nextState: RootState = workInProgress.memoizedState;
   const root: FiberRoot = workInProgress.stateNode;
   pushRootTransition(workInProgress, root, renderLanes);

   // being called "element".
   const nextChildren = nextState.element;

   // Root is not dehydrated. Either this is a client-only root, or it
   // already hydrated.
   resetHydrationState();
   if (nextChildren === prevChildren) {
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
   }
   reconcileChildren(current, workInProgress, nextChildren, renderLanes);

   return workInProgress.child;
}
reconcileChildren 会给 child 属性赋值。child 指向的是 nextChildren 通过 createFiberFromElementcreateFiberFromTypeAndProps 创建的 Fiber 节点。

至此第一轮循环结束,循环过程如下图:

beginWork循环

下次循环中,workInProgress 指向 App 的 FiberNode。因为 workInProgresstag 是 0,表示 Fiber 是函数组件,所以 beginWork 会调用 updateFunctionComponent() 进行处理。

updateFunctionComponent() 会使用 renderWithHooks 执行 App 函数,获得 App 函数中返回的 ReactElement 元素。之后将 ReactElement 元素交给 reconcileChildren 进行处理。

reconcileChildren 函数调用之前的 ReactElement 结构如下:

App 返回的 ReactElement

进入 reconcileChildren 创建 div ReactElementFiberNode 节点:

Div 的 Fiber 结构

下次循环中,workInProgress 指向 div 的 FiberNode,div FiberNodependingProps 属性表示了子节点。然后根据 pendingProps 进入子节点的构建。

由于 div 的 children 是数组,所以调用 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;

  for (let i = 0; i < newChildren.length; i++) {
    const newFiber = updateSlot(returnFiber, oldFiber, newChildren[i], lanes);

    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;  // 第一个子节点
    } else {
      previousNewFiber.sibling = newFiber;  // 设置 `sibling`
    }

    newFiber.return = returnFiber;  // 设置 `return`
    previousNewFiber = newFiber;
  }

  return resultingFirstChild; // 返回第一个节点
}

此时的 FibeNode 结构如下:

多子节点的处理方式

提交阶段

下次循环中,workInProgress 指向 H1 的 FiberNode。因为 H1 的 children 是文本,所以在 beginWork 中会把 flags 改为 Forked(Number(1048576).toString(2)) 。

因为 H1 已经没有子节点需要处理,所以会进入 completeUnitOfWork 阶段。在 complete 阶段,处理完 H1 的 DOM Element 后,会把 workInProgress 设置为兄弟节点 P。

由于 P 已经没有子节点需要处理,所以会进入 completeUnitOfWork 阶段。

function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;

    // completeWork: 完成当前节点的工作
    completeWork(current, completedWork, renderLanes);

    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // 有兄弟节点,处理兄弟节点
      workInProgress = siblingFiber;  // 移动到兄弟节点
      return;
    }

    // 没有兄弟节点,返回到父节点
    completedWork = returnFiber;
    workInProgress = completedWork;  // 移动到父节点
  } while (completedWork !== null);
}

completeWork 主要是创建 DOM 节点,设置插入 flags。对于 workInProgress 是 H1 FiberNode 的情况来说,completeWork 会将 H1 的 DOM Element 设置到 workInProgress.stateNode

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): null | Fiber {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case HostComponent:
      // 创建 DOM 节点
      const instance = createInstance(workInProgress.type, newProps);
      workInProgress.stateNode = instance;
      // 标记需要插入
      workInProgress.flags |= Placement;
      break;
    // ... 其他类型
  }

  return null;
}

处理完 H1 和 P,开始向上回溯,到父 div 的 FiberNode。通过 nextResource.appendChild(_currentHostContext.stateNode); 把 h1 和 p 插入到新创建的 div 中。 completeWork 中会标记以下属性:

之后 workInProgress 指向 App 的 FiberNode。因为它是函数组件,所以 completeWork 中会标记两个属性:

workInProgressHostRootFiber(即 App 的父级)时同样做上述处理。

到此回溯完毕,设置 workInProgressRootExitStatus = RootCompleted

Fiber构建完整流程

提交阶段

渲染完成后,检查是否可以提交:

function commitRootWhenReady(
  root: FiberRoot,
  finishedWork: Fiber,
  // ...
) {
  // 立即提交
  commitRoot(/*...*/);
}
function commitRoot(
  root: FiberRoot,
  finishedWork: Fiber,
  lanes: Lanes,
  // ...
): void {
  // 1. 刷新被动效果
  flushPendingEffects();

  // 2. 标记根节点完成
  markRootFinished(root, lanes, remainingLanes);

  // 3. 重置工作状态
  workInProgressRoot = null;
  workInProgress = null;

   // Flush synchronously.
   flushMutationEffects();
   flushLayoutEffects();
   // Skip flushAfterMutationEffects
   flushSpawnedWork();

}

DOM 节点插入

flushMutationEffects() 中调用下面的方法处理 DOM 节点的插入:

function flushMutationEffects(): void {
   commitMutationEffects(root, finishedWork, lanes);
   // --commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
   // ----recursivelyTraverseMutationEffects(root, finishedWork, lanes);
   // ------commitMutationEffectsOnFiber(child, root, lanes);
   // --------commitReconciliationEffects(finishedWork);
   // ----------commitHostPlacement(finishedWork);
   // ------------commitPlacement(finishedWork);

   root.current = finishedWork;
   pendingEffectsStatus = PENDING_LAYOUT_PHASE;
}

function commitPlacement(finishedWork: Fiber): void {
  // 1. 向上查找宿主父节点
  let parentFiber = finishedWork.return;
  while (parentFiber !== null) {
    if (isHostParent(parentFiber)) {
      hostParentFiber = parentFiber;
      break;
    }
    parentFiber = parentFiber.return;
  }
  switch (hostParentFiber.tag) {
     case HostRoot:
     case HostPortal: {
        // 2. 获取插入位置
        const before = getHostSibling(finishedWork);
        // 3. 插入节点
        insertOrAppendPlacementNodeIntoContainer(
                finishedWork,
                before,
                parent,
                parentFragmentInstances,
        );
     }
  }
}
function insertOrAppendPlacementNodeIntoContainer(
  node: Fiber,
  before: ?Instance,
  parent: Container,
  parentFragmentInstances: null | Array<FragmentInstanceType>,
): void {
  const {tag} = node;
  const isHost = tag === HostComponent || tag === HostText;

  if (isHost) {
    // 直接插入宿主节点
    const stateNode = node.stateNode;
    if (before) {
      insertInContainerBefore(parent, stateNode, before);
    } else {
      appendChildToContainer(parent, stateNode);
    }
    return;
  }

  // 递归处理子节点
  const child = node.child;
  if (child !== null) {
    insertOrAppendPlacementNodeIntoContainer(child, before, parent, /*...*/);
    let sibling = child.sibling;
    while (sibling !== null) {
      insertOrAppendPlacementNodeIntoContainer(sibling, before, parent, /*...*/);
      sibling = sibling.sibling;
    }
  }
}

页面渲染完毕,然后在 flushMutationEffects() 将 root.current 指向 FiberRoot。如下图中粉色区域进行了标注。

双缓存机制

总结

React 的初始化流程可以概括为以下几个关键步骤:

  1. 创建阶段:创建 FiberRoot 和根 Fiber 节点,初始化更新队列
  2. 调度阶段:将更新加入调度队列,通过微任务触发工作循环
  3. 渲染阶段
    • prepareFreshStack: 创建 workInProgress
    • workLoop: 遍历 Fiber
    • beginWork: 处理节点,构建子节点树
    • completeWork: 完成节点,创建 DOM 节点
  4. 提交阶段
    • commitRootWhenReady: 检查是否可以提交
    • commitRoot: 执行提交
    • commitPlacement: 插入 DOM 节点

问题

每次在 prepareFreshStack 中都会创建一个新的 WIP 树吗?