React源码解读

118 阅读29分钟

Fiber结构

export type Fiber = {
  // 一个“实例”在所有组件版本之间共享。我们可以轻松地将其拆分为一个独立对象,以避免向树的备用版本复制过多内容。
  // 目前我们将其放在单个对象上,以最小化初始渲染期间创建的对象数量。

  // 标识 fiber 类型的标签。
  tag: WorkTag,

  // 此子节点的唯一标识符。
  key: null | string,

  // element.type 的值,用于在协调此子节点时保持其身份。
  elementType: any,

  // 与此 fiber 关联的已解析的函数/类。
  type: any,

  // 与此 fiber 关联的本地状态(例如,对于DOM元素是DOM节点,对于类组件是实例)。
  stateNode: any,

  // 概念上的别名
  // parent(父): Instance -> return(返回) 由于我们合并了fiber和实例,所以父节点恰好与返回fiber相同。

  // 剩余的字段属于 Fiber

  // 处理完此 fiber 后要返回的 Fiber。
  // 这实际上是父级,但可能存在多个父级(两个),因此这仅是当前正在处理内容的父级。
  // 它在概念上等同于堆栈帧的返回地址。
  return: Fiber | null,

  // 单链表树结构。
  child: Fiber | null, // 第一个子 fiber
  sibling: Fiber | null, // 下一个兄弟 fiber
  index: number, // 在父fiber子节点列表中的索引

  // 上次用于附加此节点的 ref。
  // 为了生产环境,我将避免添加owner字段,并将其建模为函数。
  ref:
    | null
    | (((handle: mixed) => void) & {_stringRef: ?string, ...})
    | RefObject,

  refCleanup: null | (() => void), // 用于清理 ref 的函数

  // 输入是处理此 fiber 时传入的数据。参数。Props。
  pendingProps: any, // 本次渲染待处理的 props
  memoizedProps: any, // 上次渲染用于创建输出的 props

  // 状态更新和回调函数的队列。
  updateQueue: mixed,

  // 用于创建输出的状态
  memoizedState: any,

  // 此 fiber 的依赖项(上下文、事件等),如果有的话
  dependencies: Dependencies | null,

  // 描述此 fiber 及其子树属性的位字段。
  // 例如,ConcurrentMode 标志指示子树是否应默认为异步模式。
  // 创建 fiber 时,它会继承其父级的模式。
  // 创建时可以设置其他标志,但在此之后,该值应在 fiber 的整个生命周期内保持不变,尤其是在创建其子 fiber 之前。
  mode: TypeOfMode,

  // 副作用(Effect)
  flags: Flags, // 记录此 fiber 上需要执行的副作用类型(如 Placement、Update、Deletion)
  subtreeFlags: Flags, // 此 fiber 的子树中存在的副作用标志的集合
  deletions: Array<Fiber> | null, // 记录此 fiber 子树中需要被删除的子 fiber 数组

  // 优先级相关
  lanes: Lanes, // 此 fiber 所属的调度车道(优先级)
  childLanes: Lanes, // 子 fiber 的调度车道

  // 这是一个 Fiber 的复用版本。每个被更新的 fiber 最终都会有一个对应的配对节点。
  // 在某些情况下,如果需要节省内存,我们可以清理这些配对节点。
  alternate: Fiber | null, // 指向 current 树和 workInProgress 树中对应节点的指针,用于双缓存技术。

  // 为当前更新渲染此 Fiber 及其子节点所花费的时间。
  // 这告诉我们这棵树利用 sCU 进行记忆化的效果如何。
  // 每次渲染时它都会重置为 0,并且仅在我们没有“保释”时更新。
  // 仅当 enableProfilerTimer 标志启用时才会设置此字段。
  actualDuration?: number,

  // 如果此 Fiber 当前正处于“渲染”阶段,
  // 这会标记工作开始的时间。
  // 仅当 enableProfilerTimer 标志启用时才会设置此字段。
  actualStartTime?: number,

  // 此 Fiber 最近一次渲染的持续时间。
  // 当我们因记忆化目的而“保释”时,不会更新此值。
  // 仅当 enableProfilerTimer 标志启用时才会设置此字段。
  selfBaseDuration?: number,

  // 此 Fiber 所有子代的基本时间总和。
  // 此值在“完成”阶段向上冒泡。
  // 仅当 enableProfilerTimer 标志启用时才会设置此字段。
  treeBaseDuration?: number,

  // 概念上的别名
  // workInProgress(工作中) : Fiber -> alternate(备用) 用于复用的备用节点恰好与工作中的相同。
  // 仅用于 __DEV__ 开发模式

  _debugInfo?: ReactDebugInfo | null,
  _debugOwner?: ReactComponentInfo | Fiber | null,
  _debugStack?: string | Error | null,
  _debugTask?: ConsoleTask | null,
  _debugNeedsRemount?: boolean,

  // 用于验证 Hook 的顺序在渲染之间是否发生改变。
  _debugHookTypes?: Array<HookType> | null,
};

1.createRoot

一句话概括:创建root,并标记为根节点,返回root

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <App title="合一" />
  </StrictMode>
);

createRoot方法内部通过调用createContainer方法创建了一个root对象,又通过markContainerAsRoot方法将root标记为根节点。

export function createRoot(){
     //1.创建了一个root对象
    const root = createContainer(
        container,// document.getElementById("root")!
        ...
  );
   // 2.标记为根节点
   markContainerAsRoot(root.current, container);
   //3.
  return new ReactDOMRoot(root);
}

2.createContainer

createContainer方法返回一个createFiberRoot()创建的对象

export function createContainer(){
    return createFiberRoot(
        containerInfo,
        tag,
        hydrate,
        ...
  );
}

3.createFiberRoot

1.返回一个通过new FiberRoot创建的对象,2.通过initializeUpdateQueue初始化更新队列

  1. 通过new FiberRootNode创建一个root对象
  2. 通过createHostRootFiber创建一个当前更新的Fiber树,可以有多个
  3. initialState挂载到未初始化的Fiber上,记录当前的状态
  4. 通过initializeUpdateQueue初始化更新队列
export function createFiberRoot(){
    //1.通过`new FiberRootNode`创建一个root对象
   const root = new FiberRootNode(
        containerInfo,
        tag,
        hydrate,
        ...
  );
  // 2.通过`createHostRootFiber`创建一个当前更新的那个Fiber
  const uninitializedFiber = createHostRootFiber(tag, isStrictMode);
  // 3.
  const initialState: RootState = {
    element: initialChildren,
    isDehydrated: hydrate,
    cache: initialCache,
  };
  //`initialState`挂载到未初始化的Fiber上,记录当前的状态
  uninitializedFiber.memoizedState = initialState;
 }
 //4 .初始化更新队列
 initializeUpdateQueue(uninitializedFiber);
 //5.
 return root;

createHostRootFiber

return createFiber() ,创建并返回一个 HostRoot 类型的 Fiber 节点。

export function createHostRootFiber(
  tag: RootTag,
  isStrictMode: boolean,
): Fiber {
  let mode;
  if (disableLegacyMode || tag === ConcurrentRoot) {
    mode = ConcurrentMode; //启用并发特性(如时间切片、可中断渲染、过渡),提高交互与调度的弹性。
    if (isStrictMode === true) {
      mode |= StrictLegacyMode | StrictEffectsMode;
    }
  } else {
    mode = NoMode; // Legacy 行为,不启用并发与严格增强。
  }

  if (enableProfilerTimer && isDevToolsPresent) {
    // Always collect profile timings when DevTools are present.
    // This enables DevTools to start capturing timing at any point–
    // Without some nodes in the tree having empty base times.
    mode |= ProfileMode;
  }

  return createFiber(HostRoot, null, null, mode);
}

mode类型

  • ConcurrentMode :启用并发特性(如时间切片、可中断渲染、过渡),提高交互与调度的弹性。
  • NoMode :Legacy 行为,不启用并发与严格增强。
  • StrictLegacyMode :开发环境下的严格模式旧行为,帮助暴露不安全的副作用与生命周期用法(如重复调用某些生命周期/副作用以检验幂等性)。
  • StrictEffectsMode :严格执行副作用检查与行为一致性(开发环境下可能导致 useEffect 等二次调用,以检测副作用是否安全)。
  • ProfileMode :记录渲染耗时、基线时间等性能数据,使 DevTools 能随时开始采样而不会出现空的基线时间。

createHostRootFiber 给你“树的根节点(Fiber)”,负责承载应用元素状态与参与遍历。 createFiberRoot 给你“调度与容器(FiberRoot)”,负责连接宿主环境、管理优先级与渲染生命周期。

  • 两者通过 root.current 和 hostRootFiber.stateNode 形成双向关联:FiberRoot 管“何时渲染”,HostRoot Fiber 管“渲染什么”。

initializeUpdateQueue

fiber.updateQueue = queue,初始化根节点的updateQueue

export function initializeUpdateQueue<State>(fiber: Fiber): void {
  const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState,
    firstBaseUpdate: null,
    lastBaseUpdate: null,
    shared: {
      pending: null,
      lanes: NoLanes,
      hiddenCallbacks: null,
    },
    callbacks: null,
  };
  //关键
  fiber.updateQueue = queue;
}

render

createRoot(document.getElementById("root")!).render(<App/>)

createRoot返回一个对象,render方法是挂载在这个对象的原型上的,内部主要调用了updateContainer

ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
  function(){
      updateContainer(children, root, null, null);
  }

updateContainer

  1. 通过requestUpdateLane创建lan模型并返回
  2. 调用 updateContainerImpl 方法
export function updateContainer(){
  //1.通过`requestUpdateLane`创建lan模型
  const lane = requestUpdateLane(current);
  //2.
  updateContainerImpl(
    current,
    lane,
    ...
  );
  //3.
  return lane;
}
requestUpdateLane(current)

React 的 Lane 模型通过一个 31 位的二进制数来定义优先级,数值越小(二进制中低位为 1)的 Lane 优先级越高。下面这个表格能帮你快速了解所有预定义的 Lane 及其用途。

优先级类别代表性 Lane 常量二进制值 (示例)适用场景
最高优先级 (同步)SyncLane0b0000000000000000000000000000001必须同步执行的紧急更新,如用户输入、点击等直接影响交互的反馈。
高优先级 (用户阻塞)InputContinuousLane0b0000000000000000000000000000100连续的、不应阻塞但需及时响应的用户交互,如滚动、拖动。
默认优先级DefaultLane0b0000000000000000000000000010000最常见的普通状态更新,如网络请求返回后更新数据。
过渡优先级TransitionLanes(共16条)0b0000000000000000000000001000000(示例)非紧急的界面过渡更新,如页面视图切换,可使用 startTransition或 useTransition标记。
低优先级RetryLanesOffscreenLane例如 0b0000000010000000000000000000000重试任务或离屏(尚未显示)内容的预渲染。
空闲优先级IdleLane0b0100000000000000000000000000000完全非紧急的任务,仅在浏览器空闲时执行,如后台数据同步。

💡 理解 Lane 的运作方式

  • 位的含义:你可以将这 31 位想象成 31 条并行赛道。一位上的 1表示一个任务占用了该条车道。一个任务可以占用一条车道(一个单一的 Lane),也可以同时占用多条车道(一个 Lanes集合)。
  • 优先级比较:由于数值越小优先级越高SyncLane(二进制最低位为1)拥有最高的优先级。React 内部通过高效的位运算(如按位与 &、按位或 |)来比较、合并或筛选不同优先级的任务,这比传统的数值比较要快得多 。
  • 抢占式调度:高优先级的任务可以中断(抢占)正在执行的低优先级任务。例如,当用户点击按钮(高优先级)时,正在进行的页面过渡渲染(低优先级)会被中断,以确保界面能立即响应用户操作 。

updateContainerImpl

  1. 通过createUpdate创建update更新对象
  2. 通过enqueueUpdateupdate对象,追加到目标 Fiber 的更新队列中,通过并发入队把 lane 标到 fiber.lanes 以及 alternate.lanes。
this.lanes = NoLanes; // 与React的并发模式有关的调度概念。
this.childLanes = NoLanes; // 与React的并发模式有关的调度概念。
this.alternate = NoLanes; // Current Tree和Work-in-progress (WIP) Tree的互相指向对方tree里的对应单元
  1. 创建 Update 对象 : const update = createUpdate(lane) 会创建一个 Update 对象。它本质上是一个包含更新信息的 JavaScript 对象,最核心的属性是:

    • lane : 这次更新的优先级。
    • payload : 更新的内容(比如 setState 的新 state)。
    • next : 一个指针,用于将多个 Update 对象链接起来。
  2. 放入 Update Queue : 紧接着, enqueueUpdate 函数会将这个 update 对象添加到一个叫做 updateQueue 的队列中。这个队列存在于每个需要更新的 Fiber 节点(比如类组件或 HostRoot)的 updateQueue 属性上。

数据结构:一个环形链表

updateQueue 并不是一个真正的数组,而是一个 环形单向链表 。可以把它想象成一串用绳子串起来的珠子,并且把最后一颗珠子的绳头系在了第一颗珠子上。

  • updateQueue.pending : 这个属性是指向链表中 最后一个 update 对象的指针。
  • 链接方式 : 每个 update 对象通过它的 next 属性指向链表中的下一个 update 。因为是环形的,所以最后一个 update 的 next 会指向第一个 update 。
为什么用环形链表?
  • 高效添加 (O(1)) :当有新的更新进来时,只需要操作最后一个节点和新节点,就能把新更新插入到链表末尾,非常快。
  • 方便遍历 : 从 queue.pending.next 开始,就可以遍历整个链表,处理所有待处理的更新。
我们来详细拆解一下这个 O(1) 的“魔法”

想象一下,你有一串首尾相连的玩具火车车厢(环形链表),你只抓住 最后一节 车厢( last 指针,在 React 中是 updateQueue.pending )。

现在,你想在队尾再加一节新的车厢( newNode )。

如果是一条普通的、不连成环的火车(普通链表),并且你只抓住了第一节车厢:

你必须从第一节车厢开始,一节一节地走到最后,才能把新车厢挂上去。如果火车有100节,你就得走99步。这就是 O(n) 复杂度,效率很低。

但现在我们是环形的,并且抓住了最后一节,情况就完全不同了:

假设现在的火车是 ... -> A -> B ,其中 B 是你抓着的最后一节。因为是环形,所以 B 的下一节其实是头车 A 。

现在,新的车厢 C 来了。我们要把它变成新的最后一节。操作如下:

  1. 第一步:把新车厢 C 连接到头车 A 。 怎么找到头车 A ?很简单,它就是当前最后一节车厢 B 的下一节。 所以,我们让 C.next = B.next 。 现在 C 指向了 A 。
  2. 第二步:把旧的最后一节 B 连接到新车厢 C 。 我们让 B.next = C 。 现在 B 指向了 C , C 指向了 A ,环路接上了!
  3. 第三步:更新你手中的“最后一节” 。 现在 C 才是真正的最后一节了,所以我们更新指针,让你抓住 C 。 last = C 。 你看,整个过程只有这固定的几步操作。无论你原来的火车有3节还是300万节,添加新车厢的动作是完全一样的, 根本不需要从头走到尾 。

430f9b67-7fca-41b1-b194-61a0dc88f2cb.png

这就是 O(1) 或“常数时间复杂度” 的含义:操作的耗时与列表的长度无关,永远是那么快。React 用这种巧妙的数据结构来保证向更新队列中添加新任务时的高效率。

总结一下 :

createUpdate 创建了一个“更新包裹”( update 对象),然后 enqueueUpdate 把它挂到了 Fiber 上的 updateQueue 这条“更新流水线”(环形链表)的末尾,等待后续处理。

  1. 调和调度更新队列scheduleUpdateOnFiber
function updateContainerImpl(){
   //1.
   const update = createUpdate(lane);
   //2.
   const root = enqueueUpdate(rootFiber, update, lane);//通过并发入队把 lane 标到目标 Fiber( fiber.lanes 以及 alternate.lanes )
   //3.
   if (root !== null) {
    scheduleUpdateOnFiber(root, rootFiber, lane);
  }
}

scheduleUpdateOnFiber

scheduleUpdateOnFiber 函数,其核心职责就是扮演一个“通知者”和“标记员”的角色,确保更新的优先级( lane )被正确地记录在全局的“任务板”( root.pendingLanes )上,以便调度器能够看到并处理它。 这个过程非常像 医院的急诊分诊系统 :

  1. 病人到达 (产生更新) : 你调用 setState ,就像一个病人到达了医院急诊室。
  2. 分诊台护士评估 (获取 Lane) : 分诊台的护士( requestUpdateLane )会根据你的病情(比如是心梗还是普通感冒)给你一个紧急程度的标签,比如“危重”、“紧急”、“普通”。这个标签就是 lane
  3. 进入等候区 (更新入队) : 你拿着标签被引导到相应的等候区( enqueueUpdate ),和同样病情的其他病人一起等待。
  4. 通知总调度 (调用 scheduleUpdateOnFiber ) : 分诊护士会通知当班的护士长(总调度 Scheduler ):“来新病人了,已经分好级了!”
  5. 在中央白板上登记 (在 Root 上标记) : 护士长( scheduleUpdateOnFiber )走到医院大厅的中央白板( FiberRoot )前,在“待处理”一栏( pendingLanes )里,把这个病人的紧急级别( lane )登记上去。 这是关键一步 ,现在整个医院都知道有这个级别的病人需要处理。
  6. 医生接诊 (开始渲染) : 护士长看着白板上的所有登记,决定优先处理哪个级别的病人(比如永远先处理“危重”的),然后派出医生(开始渲染工作)去接诊。 所以, scheduleUpdateOnFiber 就好比是那个确保病人信息被准确登记到中央调度系统的关键角色,没有它,医生就不知道该去处理哪个病人了。 好的,我们用一个最常见的 setState 来完整地走一遍流程。

举例说明

function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    // 就是这里!
    setCount(count + 1); 
  };

  return <button onClick={handleClick}>{count}</button>;
}

当你点击这个按钮时,React 内部会发生以下故事:

第 1 步:分配“加急”标签 (获取 Lane)
  • 你点击了按钮,这是一个用户交互事件,React 认为它很重要,需要立即响应。
  • React 调用 requestUpdateLane ,因为它是一个离散的用户输入(如点击),所以会分配一个高优先级的 lane ,比如 InputContinuousLane 。这相当于给这个更新任务贴上了一个“加急”的标签。 第 2 步:打包更新 (创建 Update)
  • React 调用 createUpdate ,创建一个 update 对象。这个对象就像一个快递包裹:
标题
包裹内容 ( payload )count + 1 这个计算函数。
快递单 ( lane )InputContinuousLane (加急标签)
下一个包裹的地址 ( next )null (暂时没有下一个)
第 3 步:放入组件的“待办仓库” (入队 UpdateQueue)
  • React 找到 Counter 组件对应的 Fiber 节点。
  • 它把这个“快递包裹” ( update 对象) 放入这个 Fiber 节点的 updateQueue (环形链表)的末尾。这个操作非常快,就是我们之前讨论的 O(1) 操作。
第 4 步:拉响“调度铃” (调用 scheduleUpdateOnFiber )
  • 包裹放好了,React 必须通知总调度中心有新任务了。于是它调用 scheduleUpdateOnFiber
第 5 步:在总调度板上登记 (标记 Root)
  • scheduleUpdateOnFiber 内部立即调用 markRootUpdated
  • 它在整个应用的 FiberRoot 上,找到 pendingLanes 这个“总调度板”,然后把 InputContinuousLane 这个“加急”标签画上去。现在,调度中心知道有一个高优先级的任务需要处理。
第 6 步:调度员出动 (Scheduler 开始工作)
  • ensureRootIsScheduled 函数会确保调度员(Scheduler)被唤醒。
  • 调度员的 workLoop 开始运转,它查看“总调度板” ( pendingLanes ),发现了一个高优先级的 InputContinuousLane 任务。
  • 它说:“这个任务很急,我得马上处理!” 于是,React 开始从根节点进行渲染工作。
第 7 步:处理“待办仓库” (处理 UpdateQueue)
  • 当渲染工作进行到 Counter 组件时,React 会调用 processUpdateQueue 来处理它“待办仓库”里的更新。
  • 它遍历 updateQueue 链表,看到了我们之前放进去的那个“包裹”。
  • 它检查包裹上的“加急”标签 ( InputContinuousLane ),发现和当前正在处理的优先级匹配。
  • 于是,它打开包裹,执行 count + 1 这个计算,得到新的 state { count: 1 }
第 8 步:更新界面 (Commit)
  • React 完成渲染,发现 Counter 组件的 state 变了,需要更新 DOM
  • 在 commit 阶段,它把按钮里的文本从 0 修改为 1 。 至此,一次 setCount 的旅程就完成了。从用户点击到界面更新,每一步都清晰地对应了我们之前讨论的优先级管理调度机制

复杂一点的例子,列表更新

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 React 源码', completed: false },
    { id: 2, text: '写一个 Demo', completed: false },
    { id: 3, text: '喝杯咖啡', completed: false },
  ]);

  const handleToggle = (id) => {
    setTodos(currentTodos =>
      currentTodos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
      ))}
    </ul>
  );
}

function TodoItem({ todo, onToggle }) {
  console.log(`渲染 TodoItem: ${todo.text}`);
  return (
    <li 
      style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
      onClick={() => onToggle(todo.id)}
    >
      {todo.text}
    </li>
  );
}

假设我们点击了第一项 “学习 React 源码”。

第 1-5 步:更新入队与调度 (和之前类似)
  1. 触发更新 : handleToggle(1) 被调用。
  2. 创建 Update : setTodos 导致 React 创建一个 update 对象。这次的 payload 是一个函数: currentTodos => currentTodos.map(...)
  3. 入队 : 这个 update 对象被放入 TodoList 组件对应 Fiber 的 updateQueue 中。
  4. 调度 : scheduleUpdateOnFiber 被调用,在 Root 上标记 pendingLanes
  5. 开始渲染 : Scheduler 启动,React 从根节点开始 render 阶段。
第 6 步: beginWorkprocessUpdateQueue (在 TodoList 上)
  • 渲染工作进行到 TodoList 组件。
  • React 对 TodoList 执行 beginWork
  • processUpdateQueue 被调用,它找到了 updateQueue 里的那个 update 对象。
  • 它执行 payload 函数,基于旧的 todos state 计算出 新的 todos state
  • TodoList 的 memoizedState 被更新为这个新数组。
第 7 步:Diff 算法登场 (Reconciliation)

这是最关键的一步! TodoList 的 state 变了,React 需要弄清楚它的子组件( TodoItem 列表)发生了什么变化。它会拿新的 todos 数组生成的虚拟 DOM 旧的 Fiber 节点进行比较(diffing):

  1. 比较第一个 TodoItem (id: 1) :

    • 旧 Fiber :
    <TodoItem key={1} todo={{...completed: false}} ... /> 
    
    • 新 VDOM :
     <TodoItem key={1} todo={{...completed: true}} ... />
    
    • React 发现 key 相同组件类型 ( TodoItem ) 也相同。它判断:“ OK,这是同一个组件,不需要销毁重建,只需要更新它。 ”
    • 但是,它发现 props 变了( todo 对象里的 completed 属性不同)。
    • 于是,React 复用 这个 Fiber 节点,并给它打上一个 Update 的标记,表示它在稍后的 commit 阶段需要被更新。
  2. 比较第二个 TodoItem (id: 2) :

    • 旧 Fiber :
     <TodoItem key={2} todo={{...completed: false}} ... />
    
    • 新 VDOM :
      <TodoItem key={2} todo={{...completed: false}} ... />
    
    • key类型都相同。React 进一步比较 props ,发现 todo 对象 完全没变 。
    • 优化来了! React 判断:“ 这个组件和它的子树都没变,我不需要再对它进行 beginWork 了! ” 这个过程被称为 Bailout (保释/退出)。React 会直接复用旧的 Fiber 节点,跳过对这个组件的渲染工作。
  3. 比较第三个 TodoItem (id: 3) :

    • 同上, props 也没变,同样触发 Bailout,跳过渲染。 第 8 步:深入被标记的组件 ( beginWork on TodoItem id: 1)
  • 因为第一个 TodoItem 被标记了 Update ,所以 React 会继续对它执行 beginWork
  • 它接收到新的 props ( todo.completed 为 true )。
  • 它重新执行 TodoItem 函数, style 属性现在是 { textDecoration: 'line-through' }
  • 它对自己的子节点( li 和 文本)进行 diff,发现只是 li 的 style 属性变了。
第 9 步:Commit 阶段
  • 整个 render 阶段结束后,React 收集到了所有需要执行的 DOM 操作。
  • 在这个例子里,唯一的 DOM 操作就是: 找到 id 为 1 的那个 li 元素,并将它的 style.textDecoration 更新为 line-through
  • 其他 li 元素完全不会被触碰。 总结:

这个例子清晰地展示了:

  1. 状态在哪,就在哪更新 : update 被放在持有 todos 状态的 TodoList 组件上。

  2. processUpdateQueue 触发 Diff : TodoList 的状态更新,触发了 React 对其子组件列表的 Diff。

  3. Diff 的高效性 :通过 key ,React 能够识别出哪些组件是复用的,哪些是新增或删除的。

  4. Bailout 优化 :对于 props 没有变化的组件,React 会直接跳过它们的渲染,这是 React 高性能的关键。

  5. 最小化 DOM 操作 :最终,只有真正发生变化的 DOM 节点才会被修改。 所以,你只修改了一项,React 也只会去更新那一项对应的真实 DOM,非常高效。 非常好,新增一个 item 是另一个经典的 diff 场景。我们接着用 TodoList 的例子,但这次是添加一个新 todo。

新增的例子

我们有一个应用,包含一个 TodoList 组件和一个独立的 SearchInput 组件。

  • TodoList :显示一个项目列表,点击 "Add Todo" 按钮会向列表中添加一个新项目。

  • SearchInput :一个输入框,用户输入时会立即显示搜索词。输入操作被认为是高优先级的。

function TodoApp() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn Fiber' },
    { id: 2, text: 'Learn Lanes' },
  ]);
  const [searchTerm, setSearchTerm] = useState('');

  function handleAddTodo() {
    setTodos(prev => [...prev, { id: Date.now(), text: 'New Task' }]);
  }

  return (
    <div>
      {/* 高优先级输入框 */}
      <SearchInput value={searchTerm} onChange={setSearchTerm} />
      <hr />
      {/* 低优先级列表 */}
      <button onClick={handleAddTodo}>Add Todo</button>
      <ul>
        {todos.map(todo => <TodoItem key={todo.id} text={todo.text} />)}
      </ul>
    </div>
  );
}

function SearchInput({ value, onChange }) {
  return (
    <input
      value={value}
      onChange={e => onChange(e.target.value)}
      placeholder="Type here (high priority)"
    />
  );
}

function TodoItem({ text }) {
  // 为了模拟耗时操作,我们在这里空转一下
  const now = performance.now();
  while (performance.now() - now < 3) {
    // Do nothing for 3ms to simulate work
  }
  return <li>{text}</li>;
}

第 1 步:用户点击 "Add Todo" (低优先级更新)

  1. 触发更新 : handleAddTodo 被调用,执行 setTodos
  2. 创建与入队 :React 创建一个 update 对象
{
  lane:DefaultLane,
  payload:prev => [...prev, ...],
  next:null
}

  1. 这个 update 被放入 TodoApp 组件对应 Fiber 的 updateQueue 中。这是一个 O(1) 操作。
  2. 调度更新 : scheduleUpdateOnFiber 被调用。React 为这次更新分配一个 低优先级的 Lane (例如 DefaultLane ),并将其添加到 FiberRootpendingLanes 中。
  3. 请求工作循环 :React 向调度器 (Scheduler) 请求一个新的工作循环 ( workLoop ) 来处理这个待办任务。
第 2 步: workLoop 开始 - 构建 workInProgress

workLoop 的核心是 performUnitOfWork ,它一次处理一个 Fiber 节点,然后移动到下一个,形成一个深度优先遍历。

// packages/react-reconciler/src/ReactFiberWorkLoop.js

function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
  1. performUnitOfWork on TodoApp

    • beginWork 在 TodoApp Fiber 上执行。

    • processUpdateQueue :React 遍历 TodoApp 的 updateQueue ,执行 payload 函数,计算出新的 todos state

    • React 调用 render,得到新的 Virtual DOM 结构。然后,它将这个新 VDOMcurrent Fiber 树(上次渲染的结果)进行比较。

      • SearchInput :Props 没有变化,React 复用 current Fiber,并将其作为 TodoApp 的 child 。
      • button :同上,复用。
      • ul :同上,复用。
      • ul 的子节点 ( TodoItems) :
        • TodoItem 1 & 2 :React 发现 key 和 type 都匹配。它会复用 current Fiber 来创建 workInProgress Fiber。 props ( text ) 也没变。它将 TodoItem 1 设置为 ul 的 child ,然后将 TodoItem 2 设置为 TodoItem 1 的 sibling 。
        • TodoItem 3 (New) :React 发现这是一个新的 key 。它会创建一个 新的 FiberNode ,并给它打上 Placement 标记(表示这是一个需要插入 DOM 的新节点)。这个新 Fiber 被设置为 TodoItem 2 的 sibling 。
    • pendingProps :在为 TodoItem 3 创建新 Fiber 时,从 VDOM 传入的 props ( { text: 'New Task' } ) 被记录在该 Fiber 的 pendingProps 属性上。

2. performUnitOfWork on TodoItem 1 & 2 :
  • beginWork 在 TodoItem 1 上执行。它发现 props 没有变化,这是一个“Bailout”优化。 触发bailout需要满足的核心条件
条件类别具体条件简要说明
核心四条件1. Props 全等oldProps === newProps(注意是全等比较,非浅比较)。
2. Context 未变化所使用的Context值没有发生变化。
3. Fiber类型未变组件类型未改变(如div未变为p)。
4. 自身无相关更新当前组件上不存在与本次渲染优先级匹配的待处理状态更新。
  • React 会跳过这个组件的 render ,直接复用上次的子节点,然后移动到它的 sibling ,即 TodoItem 2。
  • TodoItem 2 同理。
第 3 步:中断!用户在 SearchInput 中输入

假设 workLoop 刚刚处理完 TodoItem 2,正准备开始处理 TodoItem 3。此时,用户在输入框里按了一个键。

  1. 高优先级更新 : SearchInput 的 onChange 被触发,调用 setSearchTerm
  2. 调度 :React 创建一个 update ,并为其分配一个 高优先级的 Lane (例如 InputContinuousLane )。这个 Lane 被添加到 FiberRootpendingLanes 中。
  3. shouldYield() 返回 true workLoopConcurrent 在处理每个 Fiber 后都会调用 shouldYield() 。这个函数会检查是否有更高优先级的任务(通过比较 lanes )或者是否渲染时间过长。现在,它发现了一个更高优先级的 InputContinuousLane ,于是返回 true 。
  4. 暂停工作
第 4 步:执行高优先级任务
  1. React 立即开始一个新的 workLoop ,但这次的 renderLanes 只包含高优先级的 InputContinuousLane
  2. 它从 root 开始,快速地只处理与 InputContinuousLane 相关的更新。
  3. beginWork on TodoApp : processUpdateQueue 运行时,它会跳过低优先级的 setTodos 更新,只处理高优先级的 setSearchTerm 更新。
  4. beginWork on SearchInput : props 变化了 ( value ),它会重新渲染 SearchInput 。
  5. 这个过程非常快,因为它跳过了所有 TodoItem 的处理。
  6. 高优先级任务的 workInProgress 树很快构建完成,并被提交(Commit),用户在输入框中立即看到了反馈。
第 5 步:恢复低优先级任务
  1. 高优先级任务完成后,React 发现 FiberRootpendingLanes 中还有之前那个低优先级的 DefaultLane
  2. 它启动一个新的 workLoop 来处理 DefaultLane
  3. React 从头开始创建workInProgress 树。
  • beginWork on TodoItem 3:这是一个新 Fiber ( Placement )。它会执行 TodoItem 函数组件,创建 li VDOM。它的 pendingProps ( { text: 'New Task' } ) 被用来渲染。
  • TodoItem 3 没有 child ,所以 beginWork 完成。

beginWork 的核心任务就是 比较 (diffing)。它将 render 函数返回的新 Virtual DOM 元素与 current Fiber 树上的旧子节点进行比较,然后产生带有“副作用标记”(Side-effect Tags)的 workInProgress 子节点。

这些标记就是告诉“Commit 阶段”需要执行什么 DOM 操作的指令:

  • Placement : 这是一个新节点,需要在 DOM 中 插入 它。
  • Update : 这是一个现有节点,但它的 props 或 state 变了,需要在 DOM 中 更新 它的属性。
  • Deletion : 这个节点在新 VDOM 中消失了,需要在 DOM 中 删除 它。 这些带标记的 Fiber 节点会在 completeWork 阶段被收集到一个叫做 effectList 的链表中,最终由 Commit 阶段统一执行。
第 6 步: completeWork 和 Commit
  1. completeWork 阶段 :当一个节点(和它的所有子孙节点)的 beginWork 都完成后, completeWork 会被执行。这个过程从下往上“冒泡”
  • completeWork on TodoItem 3:对于带 Placement 标记的 Fiber,它会创建真实的 DOM 节点 ( <li> ),并将 pendingProps 设置到 DOM 节点上
  • 然后它将这个新 Fiber(现在带着真实的 DOM 节点)的“副作用”( Placement )冒泡到父节点 ul 的 effectList 上。
  • completeWork 冒泡经过 TodoItem 2, 1, ul , button ... 直到 TodoApp 。
  1. Commit 阶段 :当整个 workInProgress 树都 complete 后,React 进入 Commit 阶段。
  • 它遍历 effectList ,发现 TodoItem 3 的 Fiber 有一个 Placement 副作用。
  • 它执行 DOM 操作:将新创建的 li 元素插入到 ul 的末尾。
  • pendingProps -> memoizedProps :在 Commit 阶段的最后, FiberRoot 的 current 指针会切换到刚刚完成的 workInProgress 树。此时,树上所有 Fiber 的 memoizedProps 都会被更新为 pendingProps 的值。这标志着本次更新成功完成, pendingProps 正式“转正”,成为下一次渲染的“旧 props”。 至此,一个包含中断和恢复的完整更新流程就完成了。React 通过 Fiber 链表、 workInProgress 树Lanes 模型,优雅地实现了可中断渲染和优先级调度,确保了在高负载下也能提供流畅的用户体验。
  • Render 阶段(可中断) :在这个阶段,React 在 requestIdleCallback 里只构建 Fiber 树和计算 state, 不触碰真实 DOM 。

  • Commit 阶段(不可中断) :在计算完成后,React 会一次性、同步地将所有变更应用到 DOM 上。

workLoop, performUnitOfWork, 和 shouldYield 的关系

  • workLoop 是一个 while 循环,是渲染工作的总引擎。
  • performUnitOfWork 是循环体里执行的核心函数,它的职责就是 处理好一个 Fiber 节点 。这个“处理好”包括:
    • 调用 beginWork (diff 子节点,计算新状态等)。
    • 如果 beginWork 产生了子节点,那么下一个要处理的就是这个 child Fiber
    • 如果没有子节点,就调用 completeWork ,然后寻找 siblingreturn 节点作为下一个处理对象。
  • shouldYield() 是用来决定 workLoop 是否应该暂停的“刹车”。在并发模式下 ( workLoopConcurrent ),每当 performUnitOfWork 完成一个工作单元 后, while 循环的条件就会检查 shouldYield() 。

伪代码如下:

// packages/react-reconciler/src/ReactFiberWorkLoop.js

function workLoopConcurrent() {
  // 只要还有工作要做,并且调度器没让停,就一直干
  while (workInProgress !== null && !shouldYield()) {
    // 干一个活儿 (处理一个 Fiber)
    performUnitOfWork(workInProgress);
  }
}

所以,这个过程是“ 干一个活儿,看一眼表,干一个活儿,看一眼表... ”的模式,而不是“把所有活儿干完再看表”。这保证了 React 可以非常及时地响应更高优先级的任务。

image.png

两种工作循环的源码与行为对比

这种差异直接体现在 workLoop的源码实现上:

  • 同步工作循环 (workLoopSync) :在同步模式下,循环会一次性处理完所有工作单元,不可中断
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress); // 持续工作,直到没有下一个节点
  }
}
  • 并发工作循环 (workLoopConcurrent) :在并发模式下,循环在每次处理工作单元前都会检查是否需要让出主线程。
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

这里的 shouldYield()函数是并发调度的“指挥官”,它主要根据两点做出决定:

  1. 时间片耗尽:默认一个时间片约为 5毫秒。如果工作超过5ms,shouldYield()返回 true,中断当前循环,避免阻塞浏览器渲染和用户交互。
  2. 更高优先级任务出现:即使时间片未用完,如果有更高优先级任务(如用户输入)准备执行,shouldYield()也会返回 true,让高优先级任务“插队”。

🔄 高优先级任务如何中断与“插队”

在并发模式下,高中低不同优先级的任务,其命运也各不相同。这一切都依赖于 Lane 模型的精妙设计。React 使用一个31位的二进制数来表示不同的“车道”(Lane),每条车道代表一种优先级,位数越高优先级越低。

  1. 中断低优先级任务:当一个高优先级任务(如 SyncLane代表的用户点击事件)产生时,调度中心会通过 cancelCallback取消当前正在执行的低优先级任务(将其回调函数设置为 null),从而中断正在进行的 workLoopConcurrent
  2. 执行高优先级任务:中断后,React 会安排并执行高优先级任务对应的 workLoop
  3. 重启低优先级任务:待高优先级任务执行完毕后,React 会检查是否还有未被处理的低优先级任务(即“车道”上是否还有任务),然后重新调用 ensureRootIsScheduled来调度执行之前被中断的任务。这也就是流程图中所展示的循环。

⚠️ 防范“饥饿问题”

你可能会担心,如果一直有高优先级任务,低优先级任务是否会被无限期推迟(即“饥饿问题”)。React 设计了过期时间(Timeout)机制来应对。每个低优先级任务(如 Transition更新)都有一个预设的超时时间(例如5秒)。如果任务由于一直被中断而超过这个时间还未完成,它的优先级会被提升到最高(同步级别),从而能够被立即执行,这就避免了“饥饿问题”的发生。

💡 对开发者的意义

理解这些机制有助于我们写出更高效的React代码:

  • 使用 startTransition和 useDeferredValue:对于非紧急更新(如搜索筛选、加载大量数据),使用这些并发API将它们标记为低优先级,避免阻塞用户交互。
  • 优化性能:意识到渲染是可中断的,减少不必要的组件重新渲染(使用 React.memouseMemouseCallback)仍然非常重要,因为这能缩短单个时间片内的计算量,让交互响应更快。