React 学习站
React›核心›核心

渲染与协调:从 setState 到屏幕

核心核心

一句话定义

React 的工作流是三步循环:触发(Trigger)→ 渲染(Render,构建新虚拟 DOM 并 diff)→ 提交(Commit,把 diff 应用到真实 DOM);这个「计算差异」的过程叫协调(Reconciliation),其执行架构叫 Fiber。

为什么重要

这是把 React 从「会用」推向「会想」的一篇。性能问题(kp-019)、key 机制(kp-009)、并发模型(kp-022)、记忆化的适用边界(kp-018),全部以这个模型为解释基础。

前置知识

  • kp-005 快照与 setter。
  • 树结构的基本概念。

核心概念

  • 虚拟 DOM(Virtual DOM):组件函数返回的 React 元素对象树,是真实 DOM 的轻量描述。
  • 协调 / Diff:新旧两棵虚拟 DOM 树的对比算法。
  • Fiber:React 内部的工作单元数据结构,使渲染过程可暂停、可恢复、可分优先级(React 16 重写引入,18 起全面兑现为并发能力)。
  • 触发条件:组件自身 state 变化、父组件重渲染导致其重渲染、context 值变化。
  • 提交:diff 结果一次性、同步地写入真实 DOM;之后执行 layout effects → 浏览器绘制 → passive effects(useEffect)。

原理 / 机制

三阶段全景图:

text[触发] setCount / 父组件重渲染 / context 变化
   ↓ 调度(Scheduler)给这次更新分配优先级(kp-022)
[渲染阶段 Render Phase] —— 可中断、可丢弃
   自顶向下递归调用组件函数 → 产出新元素树
   与上一棵树 diff:同 type? 复用 DOM/子树 : 重建
   ↓ 得到"最小变更集"
[提交阶段 Commit Phase] —— 同步、不可中断
   写入真实 DOM → layout effects → 浏览器绘制 → useEffect

Diff 算法的三条简化假设(也是三条规则):

  1. 元素类型不同 → 整棵子树销毁重建。<div> 换成 <span>,内部所有子元素全部重来,子组件 state 清零。
  2. 类型相同 → 复用 DOM 节点,只更新变化的属性/文本,并递归比较子节点。
  3. 同层列表按 key 匹配(kp-009)。

由假设 1 推出一个隐蔽 bug:

jsx{mode === 'input'
  ? <input placeholder="A" />
  : <input placeholder="B" />}

两者都是 <input>,DOM 复用,焦点不丢——通常正合适。但若希望切换时重置,需要 key 强制区分:

jsx<input key={mode} placeholder={mode === 'input' ? 'A' : 'B'} />

「渲染阶段可中断」是 Fiber 的核心价值:一次大更新的渲染被切成许多 Fiber 工作单元,高优先级任务(用户输入)可以插队,先提交紧急更新,稍后回来继续慢更新。这也解释了为什么组件必须纯函数(kp-003)——渲染随时可能重头来。

直观类比

虚拟 DOM 像装修图纸:改家具不需要砸墙重来,比较新旧图纸(diff)、只派工人动该动的房间(commit)。Fiber 像可暂停的施工队:老架构(Stack reconciler)一开工必须从头干到底,用户觉得卡;Fiber 把工程拆成无数小任务单,接到紧急电话(输入事件)可以先放下瓦刀去开门,回来接着干。

实例 / 案例

用渲染模型解释三个常见现象:

jsx// 1. "点击没反应":父组件重渲染但子组件收到的 props 没变
//    → 子组件仍会重渲染(默认行为),这是 kp-019 的优化对象
// 2. "状态莫名重置":条件分支改变了元素类型 → 子树重建 → state 清零
{showChart ? <ChartPanel /> : <TablePanel />}
//    ChartPanel/TablePanel 即使内容相似,也是不同组件 → 各自独立 state
// 3. "commit 为什么同步":保证浏览器看到的是完整一致的一帧 UI

React DevTools Profiler 能按一次 commit 展示「哪些组件渲染了、为什么、耗时多少」——调试性能的第一个入口。

常见误区

  • 「虚拟 DOM 快是因为 diff 快」:更准确的说法是它让 React 可以用 JS 对象计算最小变更,避免了手动 DOM 操作的低效与遗漏;真正的性能杠杆在「少渲染」与「正确复用」,不是「diff 有魔法」。
  • 以为 state 变化会「精确地只重渲染相关组件」:默认情况下,父组件重渲染会连带所有子组件重渲染(除非 memo,kp-018/019)。
  • 把渲染阶段副作用当可靠:渲染可能被丢弃重来;任何「只想执行一次」的逻辑放 effect 或事件。
  • 混淆渲染与提交:useLayoutEffect 在提交后、绘制前;useEffect 在绘制后。布局抖动问题的根源常在于选错(kp-010)。

自测题

  1. 写出三阶段流程,并标注哪一阶段可中断、为什么可中断(组件纯函数)。
  2. <div><p>a</p></div> 改为 <span><p>a</p></span>,p 的 DOM 会复用吗?为什么?
  3. 同一个组件在两个分支里渲染,切换分支时 state 保留还是清零?如果两个分支用的是同一个组件({flag ? <Panel mode="a"/> : <Panel mode="b"/>})呢?(保留——元素类型相同;加不同 key 才会重置。)
  4. 为什么「渲染阶段可中断」在 React 16 之前不可能实现?

与其他知识点的关系

  • 向前:是 kp-005 setter 之后故事的后半段。
  • 向后:kp-009 深入第三条 diff 假设;kp-019 把模型变成性能排查手册;kp-018 的 memo/useMemo 全部作用在渲染阶段;kp-022 展开 Fiber 调度与优先级。

延伸阅读

  • Render and Commit(官方三阶段):<https://react.dev/learn/render-and-commit>
  • Fiber 架构笔记(A. Clark):<https://github.com/acdlite/react-fiber-architecture>