普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月24日首页

React 核心深度解析:调度、协调与提交的闭环全解

2026年2月24日 15:21

React的更新机制

Trigger --> Schedule --> Render --> Commit

  • Trigger (触发):

setState触发状态变化

  • Schedule (调度):

根据优先级排队,决定什么时候开始更新

  • Render (协调/对比):

负责在内存中计算出一棵新的 Fiber 树(WIP 树),通过 Diff 算法找出所有需要变更的“标记(Flags)”

  • Commit (提交渲染):

把 Render Phase 的计算结果真正同步到真实 DOM

具体更新操作

Render
阶段一Render(协调
  • 特点: 异步、可中断。
  • 目标: 生成一颗新的 Fiber 树(WIP),并计算出需要更新的标记。
① 初始化阶段 (Initialization)
  • 核心函数:调用 prepareFreshStack
  • 做了什么
    • 创建 WIP 树的根节点:通过 createWorkInProgress(root.current, ...) 克隆出第一个 Fiber。
    • 设置全局变量 workInProgress:让它指向这个刚创建的根节点。此时,WIP 只有一个初始节点
② 下行阶段 (Downward - “铺路”与“标记”)
  • 核心函数beginWork
  • 做了什么这是 WIP 树生长的核心。
    • Diff 算法(协调) :对比当前节点的旧 Fiber 和新的 React Element。
    • 动态生成子节点:调用 reconcileChildFibers 现场创建出子 Fiber 节点,并将其连接到 WIP 树上。
    • 打上补丁标记 (Flags) :如果发现节点变了(比如文字改了、位置动了),就在这个 WIP 节点上标记 Placement(新增)或 Update(更新)。
    • 推进指针:返回刚建好的子节点。workLoop 会让指针跳到子节点上,循环执行下一轮 beginWork
③ 上行阶段 (Upward - “收尾”与“汇总”)
  • 核心函数completeWork
  • 触发时机:当 beginWork 返回 null(即当前分支已经修到叶子节点,没路了)时开始回溯。
  • 做了什么
    • 创建 DOM 实例:如果是第一次创建的节点,会调用 createElement 创建真实 DOM 并挂在 fiber.stateNode 上。
    • 属性初始化:处理 props,比如把 className 变成 DOM 的属性,但此时不挂载到页面上。
    • 副作用冒泡 (Flag Bubbling) :这是最重要的一步!它把子节点的 flags(变动标记)全部合并到父节点的 subtreeFlags 里。
    • 寻找兄弟:如果有兄弟节点,指针跳到兄弟节点,重新进入 ② 阶段;如果没有兄弟,继续向上回溯执行父节点的 completeWork
④ 完成阶段 (Completion)
  • 关键点:当 workInProgress 指针重新回到根节点并完成 completeWork 时,循环结束。
  • 结果:此时,内存中已经诞生了一棵完整的、被打满了变动标记的 WIP Fiber 树
  • 交接棒:React 将这棵树命名为 finishedWork,准备交给 Commit 阶段 去执行DOM 操作。

Commit
阶段二Commit(提交)
  • 特点: 同步、不可中断。
  • 目标: 操作真实 DOM,执行生命周期/Hooks 副作用。

当 Render Phase 结束,workLoop 退出,React 会拿着 WIP 树进入 Commit阶段。它会依次执行以下三个阶段

① Before Mutation 阶段 (DOM 变更前)
  • 核心函数: commitBeforeMutationEffects
  • 做了什么: 调用 getSnapshotBeforeUpdate 生命周期函数;处理 useEffect 的异步调度。
    • useEffect 仅在 Before Mutation 阶段被「调度」,实际异步执行在整个 Commit 阶段结束后
② Mutation 阶段 (DOM 变更)
  • 核心函数: commitMutationEffects
  • 做了什么: 真正操作 DOM。根据 Render 阶段打上的 Flags (Placement新增/移动, Update更新, Deletion删除) ,执行 appendChild, removeChild, commitUpdate。此时用户就能看到屏幕上的变化
③ Layout 阶段 (DOM 变更后)
  • 核心函数: commitLayoutEffects
  • 做了什么:
    • 同步生命周期/Hooks:此时真实 DOM 已经更新完毕。React 会执行 componentDidMount/Update 以及 useLayoutEffect
    • ref赋值:将最新的 DOM 实例赋值给你的 ref.current
    • 再次调度更新:如果在 useLayoutEffect 里又触发了 setState,会在这里再次发起一个新的调度任务
  • 关键点: 在这个阶段,root.current = finishedWork双缓存树的指针在这里正式完成切换。 WIP 树变成了 Current 树。

workLoop

WorkLoop 是驱动 Render 阶段不断向下执行的引擎。

// 摘自 ReactFiberWorkLoop.js
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  // 核心:只要有任务且浏览器没掉帧,就一直干活
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate;
  let next;

  // 1. 【向下推导】:调用 beginWork,生成子 Fiber
  next = beginWork(current, unitOfWork, renderLanes);

  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // 2. 【向上回溯】:如果没有子节点了,说明当前分支到底了,执行 completeUnitOfWork
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

Fiber

Fiber 既是静态的数据结构,也是动态的工作单元

Fiber 是 React 为Render阶段设计的 「任务单元 + 数据结构」,本质是:

  1. 数据结构层面:Fiber 是一个「增强版虚拟 DOM 节点」,用双向链表(有父 / 子 / 兄弟指针)替代传统单向树,每个 Fiber 节点对应一个组件 / DOM 元素,记录了组件的更新状态、优先级、要执行的操作(比如新增 / 更新);
  2. 执行层面:Fiber 是「最小渲染任务单元」,把原来「一整块渲染任务」拆成无数个 Fiber 小任务,每个任务可独立执行、暂停、重启。
总结
  • Fiber 本质:双向链表结构的「任务单元 + 虚拟 DOM 节点」;
  • 核心作用 1:把渲染拆成可中断的小任务,解决页面卡顿;
  • 核心作用 2:支持任务优先级,保证用户交互优先执行;
  • 核心作用 3:支撑双缓存树,让 DOM 更新更高效、无闪烁。
// 摘自 ReactInternalTypes.js
function FiberNode(tag, pendingProps, key, mode) {
  // 1. 实例属性
  this.tag = tag;             // 标记组件类型 (Function/Class/Host)
  this.key = key;
  this.elementType = null;    // 大部分情况下同 type
  this.type = null;           // 具体的组件函数或 DOM 标签
  this.stateNode = null;      // 对应的真实 DOM 节点或 Class 实例

  // 2. 构成 Fiber 树的物理链表结构
  this.return = null;         // 父节点
  this.child = null;          // 第一个子节点
  this.sibling = null;        // 右侧第一个兄弟节点
  this.index = 0;

  // 3. 工作属性 (数据)
  this.pendingProps = pendingProps; // 新传入的 props
  this.memoizedProps = null;        // 上一次渲染的 props
  this.updateQueue = null;          // 状态更新队列 (存放 setState 的 action)
  this.memoizedState = null;        // 上一次渲染的 state (Hooks 存放在这)

  // 4. 副作用相关
  this.flags = NoFlags;             // 记录当前节点的增/删/改标记
  this.subtreeFlags = NoFlags;      // 子树的副作用标记 (性能优化关键)
  this.deletions = null;            // 待删除的子节点

  // 5. 优先级调度 (Lane 模型)
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 6. 双缓存机制
  this.alternate = null;            // 指向 workInProgress 树或 current 树的对应节点
}

当前机制的弊端

React 的 beginWork 默认是贪婪的。只要父组件更新,React 无法确定子组件内部是否引用了会导致变化的数据,因此默认会重新执行所有子组件。

补救机制

React.memo

React.memo 是针对“组件级别”的拦截。

  • 原理:它将组件包装成一个特殊的 Fiber 类型(MemoComponent)。
  • 核心逻辑
    1. workLoop 走到被 memo 包裹的组件时,会触发 updateMemoComponent 函数。
    2. 它不再进行简单的引用比较,而是执行 shallowEqual (浅比较)
    3. 如果浅比较结果为 true(即所有 Props 的值都没变),它会立即执行 Bailout(跳过) 逻辑。
  • 效果:直接切断 beginWork 的向下递归,该组件及其子树完全不执行,直接复用上次的渲染结果。
const MemoChild = React.memo(function Child() {
  console.log("只有我的 Props 变了,你才能看到我打印");
  return <div>我是受保护的子组件</div>;
});

function Parent() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>更新父组件</button>
      {/* 此时 beginWork 走到这里,会执行 shallowEqual,发现没变,直接跳过 */}
      <MemoChild /> 
    </div>
  );
}
useMemo

useMemo 是针对“计算逻辑/数据”的拦截。

  • 原理:在 Fiber 节点的 memoizedState 链表中开辟一块空间,用于持久化存储数据。
  • 核心逻辑
    1. 存储结构:它在内存中存储一个格式为 [value, deps] 的数组。
    2. 对比更新:每次组件执行时,它会取出上次存储的 deps 与当前的 deps 进行逐项对比。
    3. 拦截逻辑:如果 deps 没变,直接从内存读取 value 并返回,不再重新执行计算函数。
  • 效果:避免了昂贵的计算逻辑在每次渲染时重复运行。
function DataList({ items, filterText }) {
  // ❌ 错误示范:只要组件重绘(哪怕是因为 filterText 无关的更新),都会重新排序
  // const sortedItems = items.sort(); 

  // ✅ 正确示范:只有 items 变化时,才重新计算排序
  const sortedItems = useMemo(() => {
    console.log("正在执行高昂的排序计算...");
    return items.sort();
  }, [items]); // 依赖项检查

  return <div> { sortedItems } </div>;
}
useCallback

useCallback 是针对“函数引用”的拦截。

  • 原理:它是 useMemo 的一个变体(语法糖),专门用于缓存函数引用。
  • 核心逻辑
    1. 存储结构:在 memoizedState 中存储 [callback, deps]
    2. 引用稳定:只要依赖项(deps)不改变,它始终返回同一个函数的内存地址。
  • 为什么需要它
    • 它的核心存在意义是为了配合 React.memo
    • 如果父组件给子组件传了一个函数,不包裹 useCallback 的话,每次父组件渲染都会生成新函数,导致子组件的 React.memo 因为发现 props.onClick 引用变了而拦截失败。
const BigButton = React.memo(({ onClick }) => {
  console.log("BigButton 渲染了");
  return <button onClick={onClick}>大按钮</button>;
});

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

  // ❌ 坑点:如果不包 useCallback,每次 App 更新都会生成一个新的 handleClick
  // 这会导致 BigButton 的 React.memo 浅比较失效(引用地址变了)
  // const handleClick = () => console.log("clicked");

  // ✅ 填坑:保证 handleClick 指向同一个内存地址,不会被setCount的更新影响
  const handleClick = useCallback(() => {
    console.log("clicked");
  }, []); 

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>触发更新App</button>
      <BigButton onClick={handleClick} />
    </div>
  );
}

总结:优化的本质

在 React 的 Fiber 架构中,所有的优化手段(memouseMemouseCallback)其实都在做同一件事:

通过牺牲一小部分内存(存储旧数据/旧引用),来换取在 beginWork 阶段执行 Bailout (跳过)的机会,从而减少 CPU 的计算负担。

❌
❌