- Published on
React 内部渲染机制
React 以其声明式的 UI 范式,极大地简化了复杂界面的开发。开发者只需描述在特定状态下 UI 应该呈现的样子,而 React 则负责高效地将这些声明转化为实际的 DOM 更新。这一过程的背后,是一套精密、高效且不断演进的内部工作管线。自 React 16 引入 Fiber 架构以来,其渲染机制变得更加强大,能够支持并发渲染 (Concurrent Rendering) 等高级特性。
渲染管线的四个核心阶段

一次 React 更新的完整生命周期,可以被抽象为四个连续的阶段。
阶段一:触发 (Trigger)
渲染的起点是状态的变更。当一个组件的 state 或 props 发生变化时(例如,通过 setState 调用),一个更新就被触发了。
- 机制: React 内部会调用
scheduleUpdateOnFiber()来标记需要更新的组件(在其对应的 Fiber 节点上)。这个过程会确定更新的优先级(例如,由用户交互触发的更新优先级更高),并最终通过ensureRootIsScheduled()来创建一个更新任务。 - 输出: 此阶段的最终产物是一个待处理的更新任务,它将被传递给下一阶段的调度器。
触发一次 React 渲染的来源是多样化的,主要包括:
useState的setter函数调用。useReducer的dispatch函数调用。ReactDOM.createRoot(rootNode).render(<App />)的初次渲染或后续调用。
阶段二:调度 (Schedule)
- 机制: 调度器本质上是一个优先级队列 (priority queue)。它接收来自 React 核心的任务,并根据任务的优先级(如交互式更新、过渡更新等)来决定它们的执行顺序。
scheduleCallback(): 这是调度器的入口函数,用于将任务(如渲染或 effect 执行)添加到队列中。workLoop(): 这是调度器内部的任务循环,它会根据优先级从队列中取出最高优先级的任务并执行它,直到队列为空或时间片用尽。
- 目的: 调度器的核心目标是确保高优先级的更新(如用户输入响应)能够优先得到处理,而低优先级的更新(如后台数据获取)则可以在浏览器空闲时执行,从而保证应用的响应性。
阶段三:渲染 (Render)
这是 React 最核心、最复杂的阶段,也称为协调 (Reconciliation)。在此阶段,React 会“计算”出两次渲染之间 UI 究竟发生了哪些变化。
- 机制: 调度器会调用
performConcurrentWorkOnRoot等函数来启动渲染阶段。React 会从应用的根 Fiber 节点开始,遍历整个组件树,为所有被标记为需要更新的组件,调用其渲染函数(或render方法),从而构建出一棵新的、代表更新后 UI 状态的 “工作进度 (work-in-progress)” Fiber 树。 - Diffing 算法: 在构建新树的过程中,React 会将其与当前已渲染的 Fiber 树进行对比 (diffing),找出两者之间的差异,并生成一个包含了所有需要执行的 DOM 操作(如增、删、改)的副作用列表 (effect list)。
Fiber 是 React 16+ 中协调算法的核心数据结构。它将原先不可中断的递归式 Virtual DOM diffing,重构为了一个可中断、可恢复的、基于链表的遍历过程。
每个 Fiber 节点代表一个工作单元,它包含了组件的类型、props、state、以及指向其父节点、子节点和兄弟节点的指针。这种结构使得 React 可以在渲染过程中暂停工作,处理更高优先级的任务,稍后再回来继续。
- 并发特性: 渲染阶段在并发模式下是异步的、可中断的。React 可以根据任务的优先级和可用的时间片,将渲染工作分解成多个小块执行,甚至在完成前被更高优先级的任务打断。
阶段四:提交 (Commit)
当渲染阶段成功完成,并生成了完整的副作用列表后,React 会进入同步的、不可中断·的提交阶段。提交阶段会将渲染阶段计算出的所有变更,一次性地应用到宿主环境(在 Web 中即浏览器 DOM)。这个过程主要由 commitRoot 函数协调:
useLayoutEffect 和 useEffect 的核心区别正在于它们在提交阶段的执行时机。
import { useState, useLayoutEffect, useEffect } from 'react';
function LifecycleDemo() {
const [count, setCount] = useState(0);
// Layout Effect 在 DOM 更新后、浏览器绘制前同步执行
useLayoutEffect(() => {
console.log('LayoutEffect: DOM has been updated, but browser has not painted yet.');
// 适合执行需要同步读取 DOM 布局并可能再次触发同步渲染的操作
}, [count]);
// Passive Effect 在浏览器绘制完成后异步执行
useEffect(() => {
console.log('Effect: Browser has painted the changes.');
// 适合绝大多数副作用,不会阻塞浏览器绘制
}, [count]);
return <div onClick={() => setCount(c => c + 1)}>Count: {count}</div>;
}