双缓冲树机制深度解析
一句话概括
React 维护两棵 Fiber 树——current 树(屏幕可见)和 workInProgress 树(后台构建),通过 alternate 指针复用节点,commit 阶段交换指针完成页面切换——这保证了用户永远看不到”渲染到一半”的残缺 UI。
核心知识点
1. 两棵树的角色
1
2
3
4
5
6
7
8
// 渲染前
root.current → currentTree (屏幕上显示的 UI)
root.current.alternate → workInProgressTree (后台草稿)
// commit 后
root.current = finishedWork; // 指针交换!
// 原来的 workInProgress 变成 new current
// 原来的 current 变成 new alternate(下次更新复用)
灵感来源于图形学中的”双缓冲”——在后台缓冲区画完一帧,瞬间交换到前台。
2. alternate 指针的复用机制
createWorkInProgress 是核心函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function createWorkInProgress(current, pendingProps) {
let workInProgress = current.alternate;
if (workInProgress === null) {
// 首次:创建全新 Fiber
workInProgress = createFiber(current.tag, pendingProps, ...);
workInProgress.alternate = current;
current.alternate = workInProgress; // 双向互指
} else {
// 复用:只重置可变字段
workInProgress.pendingProps = pendingProps;
workInProgress.flags = NoFlags; // 清空副作用标记
workInProgress.subtreeFlags = NoFlags;
}
// 从 current 复制不可变字段
workInProgress.type = current.type;
workInProgress.key = current.key;
workInProgress.stateNode = current.stateNode; // DOM 节点复用
return workInProgress;
}
关键优化:type、key、stateNode 这些不变的值直接从 current 复制,不需要重新计算或创建。React 的”复用”是重用 JS 对象本身,不是深层拷贝。
3. 中断安全:草稿可弃
当 Scheduler 决定让出主线程时,workInProgress 树只构建了一半。高优先级更新到来时:
1
2
3
4
5
6
7
8
如果新更新与未完成的工作有冲突(状态已变):
→ 丢弃 workInProgress 树
→ 基于 current 树重新创建
→ 从根开始重新构建
如果无冲突(如兄弟组件的更新):
→ 可以保留 workInProgress 树的部分进度
→ 但实践中 React 通常选择重建,保证一致性
无论如何,current 树始终完整——用户看到的界面不受影响。
4. effectList:只遍历有副作用的节点
在 completeWork 阶段,有 flags 标记的节点被串成一条单链表。commit 阶段只需要遍历这条链表,而不是整颗 Fiber 树:
1
2
3
4
5
6
7
8
// 5000 个节点但只有 5 个有 DOM 变更 → effectList 长度 = 5
let nextEffect = finishedWork.firstEffect;
while (nextEffect) {
if (nextEffect.flags & Placement) commitPlacement(nextEffect);
if (nextEffect.flags & Update) commitUpdate(nextEffect);
if (nextEffect.flags & Deletion) commitDeletion(nextEffect);
nextEffect = nextEffect.nextEffect;
}
5. bailout:整棵子树跳过
beginWork 中最关键的优化分支:
1
2
3
4
5
6
7
8
9
10
11
function beginWork(current, workInProgress) {
// 三个条件都满足 → 跳过
if (current !== null
&& oldProps === newProps // props 没变
&& !hasContextChanged() // context 没变
&& !includesSomeLane(renderLanes, childLanes) // 子树无更新
) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
// 直接复用 current 树的子结构,不再深入
}
}
这就是为什么 React.memo + useCallback 的组合这么重要——保持 props 引用稳定,触发 bailout。
「其实你每天都在用」
React.memo 包裹的组件:props 引用不变时,bailout 跳过整棵子树渲染。背后就是
oldProps === newProps的浅比较。useCallback 避免子组件重渲染:
handleClick引用稳定 →MemoChild的 props 不变 → bailout → 零开销。条件渲染
{show && <Heavy/>}:show 变 false 时 Heavy 的 Fiber 被标记 Deletion,DOM 销毁;show 变 true 时从零创建。双缓冲保证切换瞬间完成。在 React DevTools 的 Components 面板高亮:每当你点击一个组件,面板显示的是 current 树的结构,alternate 树只在更新期间存在于内存中。
Suspense fallback 切换:挂起时 workInProgress 树暂停构建,current 树继续显示 fallback,数据到达后 workInProgress 树恢复构建→提交→切换。
常见误解 (FAQ)
❌ 误区 1:”双缓冲就是两棵完整的 DOM 树”
双缓冲的是 Fiber 树(JS 对象),不是 DOM 树。DOM 只有一棵,始终是 current 树中 stateNode 映射的那棵。workInProgress 树中的 DOM 节点在 commit 前不插入文档。
❌ 误区 2:”每次渲染都创建全新的 Fiber 树”
通过 alternate 复用,大多数更新只重置 pendingProps 和 flags 等可变字段,对象创建量远小于整棵树的大小。真正新建的情况只有:mount 首次渲染、节点 type 变化导致无法复用。
❌ 误区 3:”两棵树各占一倍内存”
通过 alternate 指针双向引用,两棵树共享了大量不可变数据(type、key、stateNode),实际内存增量远小于 2 倍。
一句话总结
双缓冲的本质是”用内存换安全”——多一棵树的成本,换来永远不暴露中间状态给用户的承诺。