2025年03月12日10 分钟
React 渲染过程深度解析:从 JSX 到真实 DOM
深入 React 的渲染机制,详解 Fiber 架构、Reconciliation 算法、双缓冲和 Commit 阶段
React 渲染的全景图
JSX 代码
│
▼ Babel/SWC 编译
React.createElement() 调用
│
▼
React Element(虚拟 DOM 对象)
│
▼ React 内部处理
┌────────────┐
│ Render 阶段 │ 构建 Fiber 树 + Diff 算法
│ (可中断) │ 确定哪些 DOM 需要变更
└─────┬──────┘
│ 生成 Effect List
▼
┌────────────┐
│ Commit 阶段 │ 执行 DOM 操作
│ (不可中断) │ 执行生命周期/Hooks
└─────┬──────┘
│
▼
真实 DOM 更新
第一步:JSX → React Element
// 你写的 JSX
function App() {
return (
<div className="app">
<h1>Hello</h1>
<Counter count={0} />
</div>
)
}
// Babel 编译后
function App() {
return React.createElement(
'div',
{ className: 'app' },
React.createElement('h1', null, 'Hello'),
React.createElement(Counter, { count: 0 })
)
}
// React.createElement 返回的对象(React Element)
{
$$typeof: Symbol(react.element),
type: 'div',
props: {
className: 'app',
children: [
{ $$typeof: Symbol(react.element), type: 'h1', props: { children: 'Hello' } },
{ $$typeof: Symbol(react.element), type: Counter, props: { count: 0 } },
]
},
key: null,
ref: null,
}
React Element 只是一个普通的 JS 对象,描述了"我想要什么样的 UI"。它不是 DOM 节点,也没有任何实例方法。
第二步:React Element → Fiber
什么是 Fiber?
Fiber 是 React 16 引入的新架构。每个 React Element 对应一个 Fiber 节点,Fiber 节点形成一棵树。
React Element 树(描述结构): Fiber 树(工作单元):
div FiberNode(div)
/ \ │ child
h1 Counter ▼
FiberNode(h1)
│ sibling
▼
FiberNode(Counter)
Fiber 节点的结构
interface FiberNode {
// === 静态结构 ===
tag: number // 组件类型(函数组件=0, 类组件=1, 原生DOM=5...)
type: any // 'div' | 'h1' | Counter(函数引用)
key: string | null
// === 树结构(链表)===
return: FiberNode // 父节点
child: FiberNode // 第一个子节点
sibling: FiberNode // 下一个兄弟节点
// === 工作单元 ===
pendingProps: object // 新的 props
memoizedProps: object // 上次渲染的 props
memoizedState: any // 上次渲染的 state(Hooks 链表)
// === 副作用 ===
flags: number // 标记:需要插入(2)、更新(4)、删除(8)...
subtreeFlags: number // 子树的副作用标记
// === 双缓冲 ===
alternate: FiberNode // 指向另一棵树上的对应节点
}
为什么用链表而不是树?
普通树结构(递归遍历,不可中断):
function traverse(node) {
process(node)
node.children.forEach(traverse) // 递归调用栈越来越深
// 无法在中间暂停!
}
Fiber 链表(循环遍历,可中断):
function workLoop() {
while (currentFiber && !shouldYield()) {
currentFiber = performUnitOfWork(currentFiber)
}
// shouldYield() 检查是否该让出主线程
// 如果要中断,记住 currentFiber,下次继续
}
第三步:Render 阶段(Reconciliation)
双缓冲机制
React 维护两棵 Fiber 树:
current 树(当前显示的) workInProgress 树(正在构建的)
div div
/ \ / \
h1 Counter h1 Counter
│ │ │ │
"Hello" count=0 "Hello" count=1 ← 更新
commit 之后
↓
current 树(更新后的) 旧树变成下次的 workInProgress
div div
/ \ / \
h1 Counter h1 Counter
│ │ │ │
"Hello" count=1 "Hello" count=0
beginWork:自上而下构建
// 对每个 Fiber 节点执行
function beginWork(current, workInProgress) {
switch (workInProgress.tag) {
case FunctionComponent:
// 执行函数组件 → 调用你的组件函数
// 执行 Hooks(useState, useEffect 等)
const children = Component(workInProgress.pendingProps)
reconcileChildren(current, workInProgress, children)
break
case HostComponent: // 原生 DOM 元素
// 处理 props,创建子 Fiber
reconcileChildren(current, workInProgress, workInProgress.pendingProps.children)
break
}
return workInProgress.child // 返回下一个要处理的节点
}
Diff 算法(reconcileChildren)
React 的 Diff 有三个关键策略来将 O(n³) 降为 O(n):
策略1:只比较同层级节点
旧: div → [A, B, C]
新: div → [A, C, D]
只在同一层比较,不会跨层移动
策略2:不同类型的元素 → 直接替换整棵子树
旧: <div><Counter/></div>
新: <span><Counter/></span>
div 变成 span → 销毁整个旧子树,创建新子树
策略3:用 key 标识列表中的元素
旧: [<li key="a"/>, <li key="b"/>, <li key="c"/>]
新: [<li key="c"/>, <li key="a"/>, <li key="b"/>]
通过 key 识别:c 移到前面,a 和 b 不变
而不是认为三个元素都变了
列表 Diff 的详细过程
旧列表: A → B → C → D
新列表: A → C → D → B
第1轮:从头比较
A === A ✅ 可复用
第2轮:头不匹配(B !== C),进入 Map 查找
建立 Map: { B: FiberB, C: FiberC, D: FiberD }
遍历新列表剩余:
C → Map 中找到 FiberC → 标记"移动"
D → Map 中找到 FiberD → 标记"移动"
B → Map 中找到 FiberB → 标记"移动"
Map 中剩余:无 → 无需删除
结果 Effect List:
FiberA: 无变化
FiberC: 移动
FiberD: 移动
FiberB: 移动
completeWork:自下而上收集
function completeWork(workInProgress) {
switch (workInProgress.tag) {
case HostComponent:
if (current !== null) {
// 更新:对比新旧 props,记录需要更新的属性
const updatePayload = diffProperties(oldProps, newProps)
// 如 [['className', 'new-class'], ['style', { color: 'red' }]]
if (updatePayload) {
workInProgress.flags |= Update // 标记需要更新
}
} else {
// 新建:创建 DOM 节点(但不插入文档)
const instance = document.createElement(workInProgress.type)
workInProgress.stateNode = instance
workInProgress.flags |= Placement // 标记需要插入
}
break
}
// 向上冒泡副作用标记
workInProgress.return.subtreeFlags |= workInProgress.subtreeFlags
workInProgress.return.subtreeFlags |= workInProgress.flags
}
第四步:Commit 阶段
Commit 阶段不可中断,一次性执行所有 DOM 操作。
Commit 阶段的三个子阶段:
┌──────────────────┐
│ Before Mutation │ 读取 DOM 快照
│ │ getSnapshotBeforeUpdate
└────────┬─────────┘
│
┌────────▼─────────┐
│ Mutation │ 执行 DOM 操作
│ │ 插入、更新、删除
└────────┬─────────┘
│
┌────────▼─────────┐
│ Layout │ DOM 已更新
│ │ useLayoutEffect
│ │ componentDidMount/Update
└────────┬─────────┘
│
▼
浏览器绘制
│
┌────────▼─────────┐
│ Passive Effects │ 异步执行
│ │ useEffect
└──────────────────┘
Mutation 阶段的 DOM 操作
function commitMutation(fiber) {
if (fiber.flags & Placement) {
// 插入新节点
const parent = fiber.return.stateNode
parent.appendChild(fiber.stateNode)
}
if (fiber.flags & Update) {
// 更新属性
const { updatePayload } = fiber
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i]
const propValue = updatePayload[i + 1]
if (propKey === 'style') {
Object.assign(fiber.stateNode.style, propValue)
} else if (propKey === 'className') {
fiber.stateNode.className = propValue
} else {
fiber.stateNode.setAttribute(propKey, propValue)
}
}
}
if (fiber.flags & Deletion) {
// 删除节点
const parent = fiber.return.stateNode
parent.removeChild(fiber.stateNode)
}
}
并发模式下的调度
React 18 的并发模式让渲染可以被中断和恢复:
高优先级更新(用户输入)vs 低优先级更新(数据加载)
场景:用户在搜索框输入,同时有大量列表需要更新
同步模式(React 17):
[======== 列表渲染 ========] [用户输入响应]
↑ 卡顿 500ms
并发模式(React 18):
[列表渲染...] 中断 [用户输入响应] [继续列表渲染...]
↑ 即时响应
// React 内部的调度循环
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress)
}
// shouldYield():检查是否有更高优先级的任务
// 如果有 → 暂停当前渲染 → 处理高优先级任务 → 回来继续
}
一次完整更新的全流程示例
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
用户点击按钮:
1. onClick 触发 → setCount(1)
2. React 创建更新对象 { state: 1 }
3. 调度器安排更新(concurrent: 根据优先级调度)
=== Render 阶段 ===
4. beginWork(Counter Fiber)
→ 执行 Counter()
→ useState 处理更新队列 → state = 1
→ 返回 React Element: { type: 'button', props: { children: 1 } }
5. reconcileChildren
→ 旧: <button>0</button>
→ 新: <button>1</button>
→ Diff: type 相同(button),children 变化(0→1)
→ 标记 button Fiber: flags |= Update
6. completeWork
→ diffProperties: [['children', 1]]
→ 向上冒泡 subtreeFlags
=== Commit 阶段 ===
7. Before Mutation: (无操作)
8. Mutation:
→ button.textContent = 1
9. Layout:
→ 切换 current 指针到新 Fiber 树
→ 执行 useLayoutEffect(如果有)
10. 浏览器绘制 → 用户看到 1
11. Passive Effects:
→ 执行 useEffect(如果有)
总结
React 渲染的核心设计:
| 阶段 | 工作内容 | 可中断? |
|---|---|---|
| JSX 编译 | JSX → createElement 调用 | 构建时 |
| Element 创建 | 生成虚拟 DOM 对象 | 同步 |
| Render(beginWork) | 执行组件、Diff 算法、构建 Fiber | ✅ 可中断 |
| Render(completeWork) | 收集副作用、创建 DOM 节点 | ✅ 可中断 |
| Commit(Mutation) | 执行 DOM 操作 | ❌ 不可中断 |
| Commit(Layout) | useLayoutEffect、生命周期 | ❌ 不可中断 |
| Passive Effects | useEffect | 异步 |