React 的渲染流程是什么?
简化版
React 渲染大致分为触发更新、Render 阶段和 Commit 阶段。Render 阶段负责生成新的 Fiber 树并计算差异,并发渲染中的工作可以被中断;Commit 阶段负责把变更提交到真实 DOM,必须同步完成。
详细版
React 更新通常由 setState、props 变化、context 变化等触发。更新开始后,React 会根据组件返回的 JSX 创建或更新 Fiber 节点。
核心阶段:
- Render 阶段:调用组件函数,计算新旧 Fiber 差异,标记需要插入、删除、更新的副作用。
- Commit 阶段:执行 DOM 修改、ref 更新、生命周期或 effect 调度。
Fiber 的意义是把渲染任务拆成可中断的小单元,让 React 在并发模式下可以暂停、恢复、丢弃低优先级渲染,提升交互响应能力。
完整版教学
一、JSX 不是直接变成 DOM
组件返回 JSX 后,React 会把它转换成 React Element。React Element 是描述 UI 的普通对象,不是真实 DOM。React 再根据这些描述对象构建 Fiber 树。
Fiber 节点不仅描述元素类型和 props,还保存状态、更新队列、父子兄弟关系、副作用标记等。可以把 Fiber 理解成 React 内部的“工作单元”和“组件实例记录”。
二、Render 阶段负责算差异
Render 阶段会从根节点开始遍历 Fiber 树,调用函数组件,得到新的子节点描述,再和旧 Fiber 对比。这个阶段不会直接修改真实 DOM。
因为不碰真实 DOM,Render 阶段可以被中断。如果有更高优先级任务,比如用户输入,React 可以暂停当前渲染,先处理更紧急的更新。
三、Commit 阶段负责提交结果
Commit 阶段会把 Render 阶段计算好的变更应用到宿主环境,在浏览器里就是修改真实 DOM。这个阶段不能被中断,否则页面可能处于半更新状态。
Commit 还会处理 layout effect、ref、DOM 插入删除等。useLayoutEffect 会在 DOM 变更后、浏览器绘制前执行;useEffect 通常在绘制后异步执行。
四、面试追问与工程落地
常见追问是“React render 是不是一定会更新 DOM”。答案是否定的。render 只是重新计算 UI 描述,最终是否改 DOM 取决于 diff 结果。组件函数执行了,不代表真实 DOM 一定变了。
还会问“为什么不要在 render 阶段做副作用”。因为 Render 阶段可能被中断、重试或丢弃,如果你在函数体里发请求、改全局变量、操作 DOM,就可能执行多次或产生不一致。副作用应该放到 useEffect 或事件处理器里。
工程里优化渲染不能只盯着 DOM。函数组件重新执行、复杂计算、子组件递归渲染也有成本。要结合 React DevTools Profiler 找出真正耗时组件,再决定是否用 memo、useMemo、虚拟列表或状态下沉/上移。
五、把触发、Render、Commit 和浏览器绘制分开
状态更新先把工作加入队列,React 随后调用组件计算下一份 UI;Render 产生 Fiber 变更标记但不触碰真实 DOM,Commit 才应用宿主变更。浏览器在 React 交还主线程后还要进行样式计算、布局、绘制和合成,所以 Commit 完成也不等于像素已经显示。
React Render:纯计算下一棵树
React Commit:写 DOM / 更新 ref / 运行 layout effect
Browser:style → layout → paint → composite
事件 / setState
↓
更新入队 → Render(计算,可重试/放弃)
↓
Commit(DOM、ref、layout effect)
↓
浏览器绘制 → passive effect 通常随后处理
假设一次更新调用 300 个组件,但 Diff 最终只发现 2 个文本节点变化,那么函数执行数和 DOM 写入数完全不是同一指标。Profiler 的 Render duration 用于找计算热点,浏览器 Performance 面板则能看到布局、绘制和长任务,二者要结合分析。
| 工作 | 典型阶段 | 能否安全产生外部副作用 |
|---|---|---|
| 调用函数组件、比较子节点 | Render | 不能,可能重试或放弃 |
| DOM 插入/删除、ref 更新 | Commit | React 统一提交 |
useLayoutEffect setup | Commit 后、绘制前 | 可以,但会阻塞绘制 |
useEffect setup | Commit 后 | 用于非布局同步,时机不作绘制硬保证 |
六、纯渲染是并发与 StrictMode 的前提
开发环境 StrictMode 会额外调用组件函数来暴露不纯渲染,也会额外执行 effect 的 setup→cleanup 测试。生产环境不会保留这些开发检查,但如果组件在 render 中改全局变量、发请求或操作 DOM,并发重试和中断仍会让结果不可预测。
需要由用户动作直接触发的副作用放事件处理器;需要与外部系统保持同步的工作放 effect;布局测量才考虑 useLayoutEffect。这一分类比把所有逻辑都塞进生命周期式 effect 更可靠。
判断锚点:组件函数只负责算“应该看到什么”;只有进入 Commit 的结果才成为界面事实,未提交 Render 必须可以无害地重做。
七、常见误区与追问
- 误区:组件 render 了就一定修改真实 DOM。 Render 只是计算描述,Diff 无变化时 Commit 可以没有对应 DOM 写入。
- 误区:Commit 完成等于浏览器已经绘制。 浏览器仍需布局、绘制和合成,二者属于不同系统阶段。
- 误区:所有 useEffect 都严格在首次绘制后执行。 React 会根据更新来源和调度处理 effect,不能把它当精确的绘制计时 API。
- 追问:Render 为什么可以被丢弃? 它尚未改变宿主环境,出现更高优先级或更新状态时可以重新计算。
- 追问:Commit 为什么不能像 Render 一样中断? DOM 和 ref 改到一半会暴露不一致界面,因此一次提交必须保持完整性。
- 追问:useLayoutEffect 与 useEffect 怎样选择? 只有绘制前必须测量或同步布局时用前者,其他外部同步优先后者。
- 追问:怎样判断性能慢在 React 还是浏览器? 用 React Profiler 看组件计算和 Commit,再用 Performance 面板看脚本、布局与绘制。
八、加强记忆
React 渲染可以记成“两步走”:Render 阶段算出变化,Commit 阶段提交变化。Fiber 让 Render 可拆分、可中断,Commit 保证页面一次性落地。
再补上触发更新在前、浏览器绘制在后,就能解释 render 不等于 DOM 变化、Commit 不等于像素已出现。