671 字
2 分钟
React 渲染流程:从 setState 到 DOM 更新
前言
React 的渲染机制是前端面试的高频考点,也是性能优化的理论基础。本文将深入剖析从 setState 调用到 DOM 更新的完整流程,揭示 React Fiber 架构的精妙设计。
React 渲染流程概览
flowchart TB
A[触发更新] --> B[调度器 Scheduler]
B --> C[协调器 Reconciler]
C --> D[构建 Fiber 树]
D --> E[Diff 算法]
E --> F[生成副作用链]
F --> G[渲染器 Renderer]
G --> H[执行 DOM 操作]
subgraph 可中断
C
D
E
end
一、触发更新
1.1 更新触发方式
React 中有多种触发更新的方式:
// 类组件this.setState({ count: this.state.count + 1 });this.forceUpdate();
// 函数组件const [count, setCount] = useState(0);setCount(count + 1);
// HooksuseReducer, useRef, useContext...1.2 更新优先级
React 18 引入优先级调度:
flowchart LR
A[用户输入] --> B[同步优先级]
C[数据获取] --> D[默认优先级]
E[分析日志] --> F[空闲优先级]
B --> G[高优先级先执行]
D --> H[中优先级]
F --> I[低优先级后执行]
| 优先级 | 类型 | 场景 |
|---|---|---|
| Immediate | 同步 | 用户输入、离散事件 |
| UserBlocking | 用户阻塞 | 悬停、点击 |
| Normal | 默认 | 网络请求、数据更新 |
| Low | 低优先级 | 分析日志 |
| Idle | 空闲 | 预加载、缓存 |
1.3 更新队列
每次 setState 创建一个更新对象:
// 更新对象结构(简化)const update = { action: { count: 1 }, // 或 updater 函数 lane: DefaultLane, // 优先级 next: null, // 链表指针};
// 挂载到 Fiber 节点fiber.updateQueue = { shared: { pending: update, // 环形链表 },};二、调度器(Scheduler)
2.1 调度原理
调度器负责安排更新任务的执行时机:
sequenceDiagram
participant U as 更新触发
participant S as Scheduler
participant B as 浏览器
participant W as WorkLoop
U->>S: scheduleCallback(priority, callback)
S->>S: 计算过期时间
S->>B: requestIdleCallback / MessageChannel
B-->>S: 空闲时回调
S->>W: 执行工作单元
W-->>S: 是否还有工作?
S->>B: 继续请求空闲时间
2.2 时间切片
React 使用时间切片避免阻塞主线程:
// 每个工作单元的时间限制(5ms)const YIELD_INTERVAL = 5;
let startTime = -1;
function shouldYield() { const elapsedTime = getCurrentTime() - startTime; return elapsedTime >= YIELD_INTERVAL;}gantt
title React 时间切片
dateFormat X
axisFormat %s
section 第一帧
Render Phase 1 :0, 5
浏览器渲染 :5, 16
section 第二帧
Render Phase 2 :16, 21
浏览器渲染 :21, 32
2.3 MessageChannel
React 使用 MessageChannel 实现调度:
const channel = new MessageChannel();const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
function schedulePerformWorkUntilDeadline() { port.postMessage(null);}
function performWorkUntilDeadline() { startTime = getCurrentTime(); // 执行工作... if (hasMoreWork && !shouldYield()) { schedulePerformWorkUntilDeadline(); }}三、Fiber 架构
3.1 Fiber 节点结构
每个组件对应一个 Fiber 节点:
function FiberNode(tag, pendingProps, key) { // 实例属性 this.tag = tag; // 组件类型 this.key = key; // key this.type = null; // 函数组件/类组件/原生标签 this.stateNode = null; // DOM 节点或组件实例
// Fiber 树结构 this.return = null; // 父节点 this.child = null; // 第一个子节点 this.sibling = null; // 下一个兄弟节点 this.index = 0; // 在父节点中的索引
// Props 和 State this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null;
// 副作用 this.flags = NoFlags; // 副作用标记 this.subtreeFlags = NoFlags; this.deletions = null;
// 替代树(用于双缓冲) this.alternate = null;}3.2 Fiber 树结构
// JSX<div id="app"> <h1>Title</h1> <p>Paragraph</p></div>
// Fiber 树结构 Fiber(div) / \ Fiber(h1) Fiber(p) | | Fiber(text) Fiber(text)flowchart TB
A[App Fiber] --> B[div Fiber]
B --> C[h1 Fiber]
B --> D[p Fiber]
C --> E[text Fiber]
D --> F[text Fiber]
A -.->|alternate| A2[App Fiber' workInProgress]
B -.->|alternate| B2[div Fiber']
3.3 双缓冲机制
React 使用双缓冲避免闪烁:
sequenceDiagram
participant C as Current Tree
participant W as WorkInProgress Tree
participant D as DOM
Note over C: 当前显示的树
Note over W: 正在构建的树
W->>W: 构建 WorkInProgress 树
W->>W: Diff 计算
W->>W: 收集副作用
Note over W: 构建完成
C-->>W: 交换指针
W->>D: 提交 DOM 更新
Note over C: WorkInProgress 变成 Current
四、协调器(Reconciler)
4.1 开始协调
协调器遍历 Fiber 树:
function performUnitOfWork(unitOfWork) { const current = unitOfWork.alternate;
// 1. 处理当前 Fiber let next = beginWork(current, unitOfWork, renderLanes);
// 2. 更新 memoizedProps unitOfWork.memoizedProps = unitOfWork.pendingProps;
// 3. 如果没有子节点,完成当前节点 if (next === null) { completeUnitOfWork(unitOfWork); } else { return next; // 返回子节点继续处理 }
return null; // 返回兄弟节点或回到父节点}4.2 遍历顺序
Fiber 树的深度优先遍历:
flowchart TB
A[App] --> B[div]
B --> C[h1]
C --> D[text: Title]
B --> E[p]
E --> F[text: Paragraph]
style A fill:#f9f
style B fill:#bbf
style C fill:#bfb
style D fill:#fbb
style E fill:#bfb
style F fill:#fbb
遍历顺序:beginWork(App) → beginWork(div) → beginWork(h1) → beginWork(text)→ completeWork(text) → completeWork(h1)→ beginWork(p) → beginWork(text) → completeWork(text) → completeWork(p)→ completeWork(div) → completeWork(App)4.3 beginWork
处理单个 Fiber 节点:
function beginWork(current, workInProgress, renderLanes) { switch (workInProgress.tag) { case FunctionComponent: return updateFunctionComponent(current, workInProgress, renderLanes); case ClassComponent: return updateClassComponent(current, workInProgress, renderLanes); case HostComponent: // div, span 等 return updateHostComponent(current, workInProgress, renderLanes); case HostText: // 文本节点 return updateHostText(current, workInProgress); // ... }}4.4 completeWork
完成 Fiber 处理,收集副作用:
function completeWork(current, workInProgress, renderLanes) { const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) { case HostComponent: { // 创建或更新 DOM 节点 if (current === null) { // mount: 创建 DOM const instance = createInstance(type, newProps); appendAllChildren(instance, workInProgress); workInProgress.stateNode = instance; } else { // update: 更新属性 workInProgress.flags |= Update; } break; } case HostText: { // 文本节点 const newText = newProps; if (current === null) { workInProgress.stateNode = createTextInstance(newText); } else if (current.memoizedProps !== newText) { workInProgress.flags |= Update; } break; } }
// 冒泡副作用 bubbleProperties(workInProgress);}五、Diff 算法
5.1 Diff 策略
React 的 Diff 算法基于三个假设:
- 不同类型元素:直接替换
- 同类型 DOM 元素:更新属性
- 同类型组件元素:更新 props
// 策略 1:不同类型,直接替换<div><span /></div> → <div><p /></div>// 删除 span,创建 p
// 策略 2:同类型 DOM,更新属性<div className="a" /> → <div className="b" />// 只更新 className
// 策略 3:同类型组件,更新 props<App name="A" /> → <App name="B" />// 重新渲染 App5.2 列表 Diff
对子元素列表的 Diff 使用 key:
// 旧列表<ul> <li key="a">A</li> <li key="b">B</li> <li key="c">C</li></ul>
// 新列表(插入 d)<ul> <li key="a">A</li> <li key="d">D</li> // 新插入 <li key="b">B</li> <li key="c">C</li></ul>Diff 过程:
flowchart TB
A[遍历新列表] --> B{key 匹配?}
B -->|匹配| C[复用 Fiber]
B -->|不匹配| D[创建新 Fiber]
C --> E[移动位置]
D --> F[标记 Placement]
E --> G{还有剩余?}
F --> G
G -->|是| A
G -->|否| H[处理删除]
5.3 key 的重要性
// 使用 index 作为 key(不推荐){ items.map((item, index) => <Item key={index} item={item} />);}
// 使用唯一 id 作为 key{ items.map(item => <Item key={item.id} item={item} />);}index 作为 key 的问题:
原列表: [a, b, c] key: [0, 1, 2]插入 d 到开头: [d, a, b, c] key: [0, 1, 2, 3]
Diff 结果:- key=0: a → d(错误地复用)- key=1: b → a(错误地复用)- key=2: c → b(错误地复用)- key=3: c(新增)
实际应该: 创建 d,移动 a, b, c六、副作用标记
6.1 副作用类型
React 使用二进制标记副作用:
// 副作用标记(部分)export const NoFlags = /* */ 0b00000000000000000000;export const PerformedWork = /* */ 0b00000000000000000001;export const Placement = /* */ 0b00000000000000000010;export const Update = /* */ 0b00000000000000000100;export const Deletion = /* */ 0b00000000000000001000;export const ChildDeletion = /* */ 0b00000000000100000000;6.2 副作用链
React 将有副作用的 Fiber 收集为链表:
// 完成协调后,副作用链结构// 按深度优先顺序连接Fiber(div, flags=Placement) └── nextEffect → Fiber(h1, flags=Update) └── nextEffect → Fiber(p, flags=Deletion) └── nextEffect → null七、渲染器(Renderer)
7.1 提交阶段
协调完成后进入提交阶段:
function commitRoot(root) { const finishedWork = root.finishedWork;
// 1. 提交前处理(如 getSnapshotBeforeUpdate) commitBeforeMutationEffects(finishedWork);
// 2. 提交 DOM 变更 commitMutationEffects(finishedWork, root);
// 3. 提交后处理(如 componentDidMount/Update) commitLayoutEffects(finishedWork, root);
// 确保后续更新在下一帧 flushSyncCallbacks();}7.2 DOM 操作顺序
flowchart LR
A[遍历副作用链] --> B[处理 Deletion]
B --> C[处理 Placement]
C --> D[处理 Update]
subgraph Deletion
B1[删除 DOM 节点]
B2[调用 componentWillUnmount]
end
subgraph Placement
C1[插入 DOM 节点]
C2[调用 componentWillMount]
end
subgraph Update
D1[更新 DOM 属性]
D2[调用 componentWillUpdate]
end
7.3 生命周期调用时机
// 挂载阶段constructor → getDerivedStateFromProps → render →componentDidMount
// 更新阶段getDerivedStateFromProps → shouldComponentUpdate → render →getSnapshotBeforeUpdate → componentDidUpdate
// 卸载阶段componentWillUnmount八、并发模式
8.1 并发渲染
React 18 的并发特性:
// 启动并发渲染ReactDOM.createRoot(document.getElementById("root")).render(<App />);
// 使用 startTransition 标记低优先级更新import { startTransition } from "react";
startTransition(() => { setSearchResults(results); // 低优先级});8.2 优先级抢占
sequenceDiagram
participant H as 高优先级更新
participant L as 低优先级更新
participant R as React
L->>R: 开始渲染低优先级
R->>R: 渲染到一半...
H->>R: 高优先级更新到达!
R->>R: 中断当前渲染
R->>R: 渲染高优先级
R-->>H: 提交高优先级结果
R->>R: 恢复低优先级渲染
R-->>L: 提交低优先级结果
8.3 Suspense
// Suspense 组件<Suspense fallback={<Loading />}> <DataComponent /></Suspense>;
// DataComponent 内部function DataComponent() { const data = use(fetchData()); // 抛出 Promise return <div>{data}</div>;}sequenceDiagram
participant R as React
participant C as Component
participant S as Suspense
R->>C: 渲染组件
C->>R: 抛出 Promise
R->>S: 显示 fallback
Note over R: 等待 Promise resolve
R->>C: 重新渲染
R->>S: 显示内容
九、性能优化
9.1 避免不必要的渲染
// 使用 React.memoconst MemoComponent = React.memo(function Component({ data }) { return <div>{data}</div>;});
// 使用 useMemo 缓存计算function Component({ items }) { const sortedItems = useMemo(() => { return items.sort((a, b) => a - b); }, [items]); return <List items={sortedItems} />;}
// 使用 useCallback 缓存函数function Parent() { const handleClick = useCallback(() => { console.log("clicked"); }, []); return <Child onClick={handleClick} />;}9.2 虚拟列表
// 使用 react-window 或 react-virtualizedimport { FixedSizeList } from "react-window";
function VirtualList({ items }) { return ( <FixedSizeList height={600} itemCount={items.length} itemSize={35}> {({ index, style }) => <div style={style}>{items[index]}</div>} </FixedSizeList> );}9.3 代码分割
import { lazy, Suspense } from "react";
// 懒加载组件const HeavyComponent = lazy(() => import("./HeavyComponent"));
function App() { return ( <Suspense fallback={<Loading />}> <HeavyComponent /> </Suspense> );}十、调试工具
10.1 React DevTools
Components 面板:├── 查看组件树├── 查看 Props/State├── 高亮更新(渲染次数)└── 追踪渲染原因
Profiler 面板:├── 录制渲染性能├── 分析组件渲染时间└── 识别性能瓶颈10.2 为什么渲染分析
// 使用 why-did-you-renderimport whyDidYouRender from "@welldone-software/why-did-you-render";
if (process.env.NODE_ENV === "development") { whyDidYouRender(React, { trackAllPureComponents: true, });}
// 标记组件Component.whyDidYouRender = true;总结
React 渲染完整流程
flowchart TB
subgraph 触发
A[setState/dispatch]
end
subgraph 调度
B[创建更新对象]
C[计算优先级]
D[加入调度队列]
end
subgraph 协调
E[构建 WorkInProgress 树]
F[执行 Diff 算法]
G[收集副作用]
end
subgraph 提交
H[提交前副作用]
I[执行 DOM 操作]
J[提交后副作用]
end
A --> B --> C --> D --> E --> F --> G --> H --> I --> J
关键要点
- Fiber 架构:可中断的渲染过程
- 调度器:优先级调度、时间切片
- 协调器:Diff 算法、副作用收集
- 渲染器:DOM 操作、生命周期
- 并发模式:优先级抢占、Suspense
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
React 渲染流程:从 setState 到 DOM 更新
https://blog.souloss.com/posts/principles/react-rendering-process/ 部分信息可能已经过时
相关文章 智能推荐
1
Kubernetes Pod 创建流程:从 kubectl 到容器运行
原理 深入剖析 Kubernetes 创建 Pod 的完整流程——从 kubectl 提交到 kubelet 启动容器,涵盖 API Server、Controller、Scheduler、Kubelet 等核心组件。
2
容器在 Kubernetes 中
容器运行时 深入容器在 Kubernetes 中的运行机制——CRI 接口、Pod Sandbox 创建、多容器模式、Init Container、Sidecar 注入,以及 kubelet 如何通过 CRI 与容器运行时交互。
3
Istio:服务网格的工程实践
分布式系统深入 深入解析 Istio 服务网格——Sidecar 代理、流量管理、安全策略与可观测性的工程实践
4
goroutine 上下文切换详解:CPU 到底做了什么?
golang 深度解析 goroutine 上下文切换的底层机制——gogo 汇编、栈切换、用户态调度与性能分析
5
eBPF 在 Kubernetes
eBPF Kubernetes 是 eBPF 技术最大的应用场景——从 CNI 网络插件到 kube-proxy 替代,从 NetworkPolicy 增强到 Ambient Mesh,eBPF 正在重塑 K8s 的网络与安全基础设施。本章详解 eBPF CNI 对比、Cilium 的 K8s 集成、Ambient Mesh 无边车模式、多集群网络,以及 eBPF 在 K8s 中的生产部署最佳实践。






