文章

双缓冲树机制深度解析

双缓冲树机制深度解析

一句话概括

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;
}

关键优化:typekeystateNode 这些不变的值直接从 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。

「其实你每天都在用」

  1. React.memo 包裹的组件:props 引用不变时,bailout 跳过整棵子树渲染。背后就是 oldProps === newProps 的浅比较。

  2. useCallback 避免子组件重渲染handleClick 引用稳定 → MemoChild 的 props 不变 → bailout → 零开销。

  3. 条件渲染 {show && <Heavy/>}:show 变 false 时 Heavy 的 Fiber 被标记 Deletion,DOM 销毁;show 变 true 时从零创建。双缓冲保证切换瞬间完成。

  4. 在 React DevTools 的 Components 面板高亮:每当你点击一个组件,面板显示的是 current 树的结构,alternate 树只在更新期间存在于内存中。

  5. Suspense fallback 切换:挂起时 workInProgress 树暂停构建,current 树继续显示 fallback,数据到达后 workInProgress 树恢复构建→提交→切换。

常见误解 (FAQ)

❌ 误区 1:”双缓冲就是两棵完整的 DOM 树”

双缓冲的是 Fiber 树(JS 对象),不是 DOM 树。DOM 只有一棵,始终是 current 树中 stateNode 映射的那棵。workInProgress 树中的 DOM 节点在 commit 前不插入文档。

❌ 误区 2:”每次渲染都创建全新的 Fiber 树”

通过 alternate 复用,大多数更新只重置 pendingPropsflags 等可变字段,对象创建量远小于整棵树的大小。真正新建的情况只有:mount 首次渲染、节点 type 变化导致无法复用。

❌ 误区 3:”两棵树各占一倍内存”

通过 alternate 指针双向引用,两棵树共享了大量不可变数据(type、key、stateNode),实际内存增量远小于 2 倍。

一句话总结

双缓冲的本质是”用内存换安全”——多一棵树的成本,换来永远不暴露中间状态给用户的承诺。

本文由作者按照 CC BY 4.0 进行授权

© 独行的风. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

本站总访问量 本站访客数 本文阅读量