🔥🔥🔥 React18 源码学习 - Render 阶段(构造 Fiber 树)

前言

本文的React代码版本为18.2.0

可调试的代码仓库为:GitHub - yyyao-hh/react-debug at master-pure

上一节讲了Fiber的结构和概念,要真正理解React的并发渲染机制,我们必须深入剖析Fiber树的构建过程。本文将从源码层面,完整解析ReactRender阶段如何构建Fiber树。

Fiber 树的构建入口

多余代码均被省略,仅保留核心逻辑

我们从入口文件开始分析:首先创建一个根容器,然后对应用进行挂载

ini 复制代码
/* index.js */

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

然后在render方法的内部调用了updateContainer方法启动了渲染流程

javascript 复制代码
/* react/packages/react-dom/src/client/ReactDOMRoot.js */

ReactDOMRoot.prototype.render = function(children: ReactNodeList) {
  const root = this._internalRoot; // 根节点
  updateContainer(children, root, null, null);
};
scss 复制代码
/* react/packages/react-reconciler/src/ReactFiberReconciler.old.js */

export function updateContainer(...) {
  const root = enqueueUpdate(current, update, lane);
  scheduleUpdateOnFiber(root, current, lane, eventTime);
}
scss 复制代码
/* react/packages/react-reconciler/src/ReactFiberWorkLoop.old.js */

export function scheduleUpdateOnFiber(...) {
  // 注册调度任务, 之后由 Scheduler 调度, 构造 Fiber 树
  ensureRootIsScheduled(root, eventTime);
}

ensureRootIsScheduled函数中通过scheduleCallbackscheduleSyncCallbackscheduleLegacySyncCallback等方法注册了两种类型的调度任务。然后调度任务中就是Fiber树的构建逻辑。

  • 同步渲染:performSyncWorkOnRoot
  • 并发渲染:performConcurrentWorkOnRoot

本章无需关心任务的调度,在调度器章节会详细分析。我们只需要了解在注册任务后,任务会在某个时机会被调度器(Scheduler)执行。

scss 复制代码
/* react/packages/react-reconciler/src/ReactFiberWorkLoop.old.js */

function ensureRootIsScheduled(...) {
  // 1. 同步任务处理
  if (newCallbackPriority === SyncLane) {
    if (root.tag === LegacyRoot) {
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }
    
  // 2. 并发任务处理
  } else {

    // ...计算优先级的逻辑
    
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }
}

render阶段正是从performSyncWorkOnRootperformConcurrentWorkOnRoot方法的调用开始。取决于本次是同步更新还是异步更新。但不论是哪种情况,最终都会调用本章的重点:performUnitOfWork方法

  • performSyncWorkOnRoot => renderRootSync => workLoopSync
  • performConcurrentWorkOnRoot => renderRootConcurrent => workLoopConcurrent
scss 复制代码
/* packages\react-reconciler\src\ReactFiberWorkLoop.old.js */

function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
} 

这两个函数唯一的区别就是是否调用了shouldYield方法:判断是否应中断当前任务。

在之后的调度器章节会详细分析。

Fiber 树的构建过程

performUnitOfWork函数是Fiber架构中工作循环(work loop)中的核心函数,负责对单个Fiber节点进行处理并驱动整个渲染流程。其核心作用是协调" "和""两个阶段,实现可中断的深度优先遍历。

javascript 复制代码
/* packages\react-reconciler\src\ReactFiberWorkLoop.old.js */

let workInProgress: Fiber | null = null;

function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork: Fiber): void {
  const next = beginWork(current, unitOfWork, subtreeRenderLanes);

  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

Fiber树的遍历采用深度优先遍历,在遍历的过程中:

  • workInProgress:指向当前正在处理的Fiber节点的指针。若指针为null,表示所有工作单元已处理完毕,结束循环
  • performUnitOfWork:创建下一个Fiber节点并赋值给指针workInProgress,并将workInProgress 与已创建的Fiber节点连接起来构成Fiber
  • next:下一个需要处理的Fiber节点。先从beginWork中获取,如果没有,就从completeUnitOfWork获取。

beginWork是"递",即深度优先遍历找到当前分支最深叶子节点的过程;

completeUnitOfWork是"归",即结束这个分支,向右或向上的过程。

然后接下来分别深入了解beginWorkcompleteUnitOfWork这两个方法

递 - beginWork

首先从RootFiberNode开始进行深度优先遍历。遍历到的每个节点都会调用beginWork方法。该方法为传入的Fiber节点创建子Fiber节点,并将两个节点进行关联,然后返回子Fiber节点。

当返回的子Fiber节点为空(叶子节点),这时就会进入"归"阶段。

beginWork方法内部主要是根据tag分发逻辑,处理不同类型的Fiber节点。将处理方法的返回的子节点作为下一个遍历节点(熔断策略会另起一篇文章讲解)

kotlin 复制代码
/* packages/react-reconciler/src/ReactFiberBeginWork.old.js */

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  if(...) {...}; // 满足一定条件下执行熔断策略

  switch (workInProgress.tag) {
    case IndeterminateComponent: return mountIndeterminateComponent(...);
    case LazyComponent: return mountLazyComponent(...);
    case FunctionComponent: return updateFunctionComponent(...);
    case ClassComponent: return updateClassComponent(...);
    case HostRoot: return updateHostRoot(...);
    case HostComponent: return updateHostComponent(...);
    case HostText: return updateHostText(...);
    case SuspenseComponent: return updateSuspenseComponent(...);
    case HostPortal: return updatePortalComponent(...);
    case ForwardRef: return updateForwardRef(...);
    case Fragment: return updateFragment(...);
    case Mode: return updateMode(...);
    case Profiler: return updateProfiler(...);
    case ContextProvider: return updateContextProvider(...);
    case ContextConsumer: return updateContextConsumer(...);
    case MemoComponent: return updateMemoComponent(...);
    case SimpleMemoComponent: return updateSimpleMemoComponent(...);
    case IncompleteClassComponent: return mountIncompleteClassComponent(...);
    case SuspenseListComponent: return updateSuspenseListComponent(...);
    case ScopeComponent: return updateScopeComponent(...);
    case OffscreenComponent: return updateOffscreenComponent(...);
    case LegacyHiddenComponent: return updateLegacyHiddenComponent(...);
    case CacheComponent: return updateCacheComponent(...);
    case TracingMarkerComponent: return updateTracingMarkerComponent(...);
  }
}

可以简单看下对类组件(ClassComponent)和函数式组件(FunctionComponent)的处理。可以看出每个函数最终都会返回workInProgress.child:因为遵循深度优先,返回节点即为当前节点的第一个子节点

javascript 复制代码
// 类组件 (ClassComponent)
function finishClassComponent(...) {
  return workInProgress.child;
}

// 函数式组件 (FunctionComponent)
function updateFunctionComponent(...) {
  const nextUnitOfWork = finishClassComponent(...);
  
  return nextUnitOfWork;
}

function finishClassComponent(...) {
  return workInProgress.child;
}
归 - completeUnitOfWork

当返回的子Fiber节点为空,就会继续调用completeUnitOfWork函数进行"归"阶段的处理。completeUnitOfWork函数内部也有一层循环,并搭配了一个新的向上的指针。判断:

  • 如果该节点有兄弟Fiber节点,就会改变workInProgress,并跳出函数内部的循环,进入兄弟Fiber节点的"递"阶段。
  • 如果该节点无兄弟Fiber节点,指针就会指向父节点,就会进入父级Fiber节点的"归"阶段。
ini 复制代码
/* packages/react-reconciler/src/ReactFiberWorkLoop.old.js */

function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do {  
    const returnFiber  = completedWork.return;
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }

    completedWork = returnFiber;
    workInProgress = completedWork;
    
  } while (completedWork !== null);
}

这个"递"和"归"的处理相互交错,直到最终回到RootFiberNode。循环结束!

completeUnitOfWork阶段还会构建effect链表,为后续的commit阶段提供精确的DOM操作指令。但这个会在之后的commit阶段中展开详细讲解。

在这个例子中:

  1. 首先进入根节点A1,执行beginWork方法。返回其子节点B1,继续循环;
  2. 进入B1,执行beginWork方法。但是B1没有子节点返回,继续执行completeUnitOfWork方法。
    1. 该节点有兄弟节点,因此使workInProgress指针指向其兄弟节点B2,继续外层循环;
  1. 进入B2,执行beginWork方法。返回其子节点C1,继续循环;
  2. 进入C1,执行beginWork方法。但是C1没有子节点返回,继续执行completeUnitOfWork方法。
    1. 该节点无兄弟节点,因此使方法内部指针指向其父节点B2,继续方法内部循环;
    2. B2节点继续内部循环,判断B2有兄弟节点,因此使workInProgress指针指向其兄弟节点B3,继续外层循环;
  1. 进入B3,执行beginWork方法。返回其子节点C2,继续循环;
  2. 进入C2,执行beginWork方法。返回其子节点D1,继续循环;
  3. 进入D1,执行beginWork方法。但是D1没有子节点返回,继续执行completeUnitOfWork方法。
    1. 该节点有兄弟节点,因此使workInProgress指针指向其兄弟节点D2,继续外层循环;
  1. 进入D2,执行beginWork方法。但是D2没有子节点返回,继续执行completeUnitOfWork方法。
    1. 该节点无兄弟节点,因此使方法内部指针指向其父节点C2,继续方法内部循环;
    2. C2节点继续内部循环,判断C2无兄弟节点,因此使方法内部指针指向其父节点B3,继续方法内部循环;
    3. B3节点继续内部循环,判断B3无兄弟节点,因此使方法内部指针指向其父节点A1,继续方法内部循环;
    4. A1节点继续内部循环,判断A1无兄弟节点无父节点,因此内层循环结束,外层循环结束;

workInProgress指针的变化(遵循深度优先):A1、B1、B2、C1、B3、C2、D1、D2、A1

completeUnitOfWork指针的变化(根据workInProgress指针的变化):

  • B1->B2B1
  • C1->B3C1、B2
  • D1->D2D1
  • D2->A1D2、C2、B3、A1

其实completeUnitOfWork函数内部的指针completedWork在兄弟节点之间并未跳转(比如B2->B3),此处连接只是为了方便大家理解~

总结

Render阶段的核心要点(本章只详细讲解了第一条,其他慢慢补坑):

  1. 深度优先遍历:Fiber树的构建采用DFS策略,确保父节点在子节点之前开始,在子节点之后完成。
  2. 可中断渲染:React的并发特性建立在可中断的Fiber架构之上,通过shouldYield控制渲染任务的中断和恢复。
  3. 副作用收集:completeUnitOfWork阶段构建effect链表,为后续的commit阶段提供精确的DOM操作指令。
  4. 高效协调:通过key优化、多轮diff算法和bailout机制,最大限度地复用现有节点。

下一章我们将了解Fiber树构建过程中的复用策略:Diff算法

相关推荐
有意义2 小时前
TypeScript 不是加法,是减法
react.js·typescript·前端框架
怕浪猫3 小时前
React从入门到出门第八章 React19新特性use()/useOptimistic 原理与业务落地
javascript·react.js·前端框架
Amumu121383 小时前
Redux介绍(一)
前端·javascript·react.js
旭日猎鹰3 小时前
配置ReactNative环境并创建第一个程序
javascript·react native·react.js
阿湯哥4 小时前
ReAct智能体
前端·react.js·前端框架
2301_789169544 小时前
ai讲React 18 + Context API 极简教程 解决深层组件调用父组件里其他组件方法
javascript·react.js·ecmascript
王同学 学出来4 小时前
React案例实操(一)
react.js
摘星编程4 小时前
React Native for OpenHarmony 实战:Easing 动画缓动函数详解
javascript·react native·react.js
2501_948122634 小时前
React Native for OpenHarmony 实战:Steam 资讯 App 浏览历史页面
javascript·react native·react.js·游戏·ecmascript·harmonyos