陌上人如玉
公子世无双

12.11 Fiber 解决了什么

12.11 Fiber 解决了什么

一句话:Fiber 把「不可中断的递归渲染」改造成「可中断的链表遍历」——从而支持时间切片、优先级调度与并发渲染,让长渲染不再阻塞用户输入。

一、结论先行

问题:Fiber 解决了什么问题?

一句话回答:
  它解决了「同步递归渲染无法中断」的问题——
  把渲染拆成「一个个工作单元」,用「链表指针」替代递归,
  因此可以「暂停 / 恢复 / 丢弃」,从而支持时间切片与优先级调度。

⭐ 三个关键词:
① 可中断(把递归改成循环 + 指针)
② 双缓存(两棵树,提交只需切换指针)
③ 优先级(Lane 位掩码 + 时间切片)

二、React 15 的问题(为什么需要 Fiber)

// React 15 的 Stack Reconciler:递归遍历整棵元素树
function reconcile(element) {
  if (element.type) {
    updateDOM(element);
    element.children.forEach(reconcile);      // ⚠️ 递归
  }
}
⭐ 问题:递归一旦开始就无法中断

① 浏览器的「帧预算」约 16.7ms(60fps),留给 JS 的更少
② React 15 的递归可能一次跑几百毫秒(大列表、深树)
   → 期间【无法让出主线程】
   → 用户输入、动画、点击全部被阻塞(掉帧、输入延迟)
③ 而且这个工作【无法被打断】:
   不能「先渲染一半、处理一下用户输入、再继续」

⭐ 一句话:React 15 的渲染是「同步且不可中断」的,
   在长渲染时会「卡住整个页面」。

三、Fiber 的核心思路

三个关键改造:

① 【把工作拆成「工作单元」】
   每个 fiber 节点就是一个「工作单元」(一个组件的渲染、一个节点的 diff)

② 【用「链表」替代「递归」】
   每个 fiber 有 return(父)/ child(长子)/ sibling(兄弟)三个指针
   → 遍历变成「循环 + 指针移动」→ 因此可以在任意一步【暂停】

③ 【加「优先级」】
   每个 fiber 有 lanes / childLanes(Lane 位掩码)
   → 高优先级的更新可以「插队」
   → childLanes 让 React 能「跳过整棵没有更新的子树」
// Fiber 节点(简化)
{
  tag, type, stateNode,                      // 组件信息(stateNode:宿主组件→DOM,类组件→实例)
  return, child, sibling,                    // ⭐ 树结构指针(替代递归)
  pendingProps, memoizedProps, memoizedState, updateQueue,   // 更新信息(Hooks 链表挂在 memoizedState)
  alternate,                                 // ⭐ 双缓存(指向另一棵树的对应节点)
  flags, subtreeFlags,                       // 副作用标记(Placement/Update/Deletion…)
  lanes, childLanes,                         // ⭐ 优先级
}
⭐ 遍历方式(用循环替代递归):

① 「递」:处理当前节点 → 如果有 child,进入 child
② 「归」:没有 child → 处理「完成」逻辑 → 
        有 sibling 就处理 sibling;没有就回到 return(父)
├── 所以整个遍历是「一个 while 循环 + 指针移动」
└── 每处理完一个单元都可以「检查是否需要让出」(可中断)

⚠️ 关键点:
   递归的调用栈「无法保存/恢复」,而「指针 + 循环」的状态
   只是一个变量(nextUnitOfWork)→ 天然可暂停。

四、双缓存树(Double Buffering)

React 在内存里同时维护两棵树:

   current 树(当前显示在屏幕上的)        workInProgress 树(正在构建的)
        ┌─────────┐                          ┌─────────┐
        │   App   │◄────── alternate ─────►  │   App   │
        └────┬────┘                          └────┬────┘
             │                                    │
        ┌────▼────┐                          ┌────▼────┐
        │  List   │◄────── alternate ─────►  │  List   │
        └─────────┘                          └─────────┘

① 每次渲染基于 current 创建/复用对应的 workInProgress 节点
② 两个节点通过 alternate 互指(可从任一棵找到另一棵)
③ 工作完成后,root.current 指向 workInProgress 树 → 提交只是一次指针切换
⭐ 双缓存的三个好处:
① 用户始终看到「一致的一棵树」(不会看到渲染到一半的状态)
② 「提交」操作只是切换指针(瞬间完成)
③ 渲染被更高优先级打断时,可以「直接丢弃」workInProgress
   → current 树完好无损 → 屏幕内容不受影响

⚠️ 另:函数组件的 Hooks 链表就挂在 fiber.memoizedState 上
   → 这解释了「为什么 Hooks 顺序必须稳定」(见 12.1)

五、渲染的两个阶段

① render 阶段(可中断 ⏸️)
   · 遍历 fiber、执行组件函数、diff、收集副作用标记(flags)
   · 【可以被打断、被丢弃、被重放】
   → 因此必须【纯】(不能有副作用)

② commit 阶段(不可中断 ▶️)
   · 把 workInProgress 上的副作用一次性应用到 DOM
   · 三个子阶段:
     a. before mutation(DOM 变更前)
     b. mutation(真正改 DOM,并执行 layout effect 的清理)
     c. layout(赋 ref、执行 useLayoutEffect)
   · 之后是浏览器绘制
   · 最后是 passive effects(异步执行 useEffect)
阶段 可中断 能做什么 典型 API
render ✅ 计算、纯函数调用 组件函数、useMemo 计算
commit:mutation ❌ 操作 DOM —
commit:layout ❌ 读/写布局、同步副作用 useLayoutEffect、ref
passive ❌(异步) 异步副作用 useEffect
⭐ 为什么 render 必须纯?
   因为它可能被「中断后重放」或「整体丢弃」——
   有副作用就会「重复执行」或「产生不一致」。
   (这也是 StrictMode 双调用的理论基础,见 8.5)

六、时间切片(Time Slicing)

① React 用独立的【Scheduler】安排工作
② 每次分配一个时间片(约 5ms)
③ 每处理完一个 fiber 单元就检查:
   · 时间片是否用完?
   · 是否有更高优先级的任务?
④ 是 → 让出主线程(保存 nextUnitOfWork 指针)
   → 浏览器处理输入/动画/绘制
   → 用 MessageChannel 安排下一次继续
⑤ 下一帧从「记录的位置」继续

⚠️ 关键:
· 可中断的是【render 阶段】;commit 阶段必须一次做完
· 只有「标记为非紧急的更新」(transition 优先级)才真正受益
  (点击等 Sync 优先级的更新仍然同步执行)
⭐ 一个常见误解要澄清:
   「Fiber 让 React 变快了」
   → 不准确。它主要提升「【响应性】」(长渲染不阻塞输入),
     而「吞吐量」未必更好(维护 fiber 与调度有额外开销)。
     准确说法:「Fiber 让 React 在长时间渲染时仍然可交互」。

七、优先级:Lane 模型

// Lane 是「位掩码」(一个 32 位整数的不同位表示不同优先级)
const SyncLane = 0b001;                    // 最高(离散事件:click/keydown)
const InputContinuousLane = 0b100;         // 连续事件(mousemove/scroll)
const DefaultLane = 0b10000;               // 默认(普通 setState、网络回调)
const TransitionLanes = 0b1110000000;      // 过渡(startTransition)
const IdleLane = 0b10000000000;            // 空闲
⭐ 为什么用「位掩码」而不是数字?
① 一个 fiber 上可能【同时存在多个待处理更新】
   → 用「按位或」就能把它们合并到一个整数里
② 判断「是否包含某类优先级」只需位运算(极快)
③ 能表达「一批车道(Lanes)」的概念

⭐ childLanes 的作用:
   记录「子树里是否有待处理更新」→
   遍历时可以【跳过整棵没有更新的子树】(性能关键)

八、常见追问

Q: Fiber 是一棵「树」还是「链表」?
A: 「用链表指针表达的树」——每个节点有 child/sibling/return,
   所以既可以「当树遍历」,又能「当链表逐步推进」。
   「链表」这个特性是可中断的关键。

Q: 为什么「可中断」很重要?
A: 因为「同步长任务会阻塞主线程」——
   可中断让 React 能在「一帧内只做一小部分」,
   把主线程还给浏览器的输入/动画/绘制。

Q: 「中断」后「之前做的工作」会丢吗?
A: 会「分情况」:
   · 如果只是「让出」→ 下次从记录的位置继续(不丢)
   · 如果「有更高优先级的更新插进来」→ 
     已完成的部分可能被【丢弃】(重新按新状态渲染)
   这正是「render 必须纯」的原因(丢弃不会留下副作用)。

Q: 双缓存树的「current」与「workInProgress」怎么切换?
A: 工作完成后把 root.current 指向 workInProgress 树
   (并让新树的每个节点「反向指向」旧树,作为下一次的 alternate)。

Q: commit 阶段为什么不能中断?
A: 因为「DOM 更新到一半」会让用户看到「不一致」的界面
   (如「列表只删了一半」)。所以 React 的选择是
   「render 可以慢慢算,commit 必须一口气做完」。

Q: 时间切片的「5ms」是固定的吗?
A: 不固定——它取决于「帧的剩余时间」与「是否有更高优先级任务」。
   React 用 Scheduler 动态判断「是否该让出」。

Q: Fiber 能「并行渲染」(多线程)吗?
A: 不能。它仍然是「单线程的可中断串行」。
   真正的并行渲染还在实验阶段(不在生产可用范围)。

Q: 如果我要「深入理解 Fiber」,从哪看起?
A: 建议顺序:① 双缓存树与三个指针;② reconcileChildren(diff 如何生成 fiber);
   ③ beginWork/completeWork(递与归);④ commitRoot 的三个子阶段;
   ⑤ lanes 与 scheduleUpdateOnFiber(优先级与调度)。

九、常见坑

① 认为「Fiber 让 React 变快了」(它主要提升响应性)
② 认为「所有更新都会被打断」(Sync 优先级的点击/输入仍然同步)
③ 在 render 阶段做副作用(可能被重放/丢弃 → 副作用重复或丢失)
④ 认为「useEffect 在 render 之后立即执行」(它在绘制后异步执行)
⑤ 认为「commit 也会被打断」(不会)
⑥ 认为「Fiber 就是虚拟 DOM」(Fiber 是「工作单元/调度结构」,
   虚拟 DOM 是「UI 描述」;fiber 里包含元素信息,但还有更多调度字段)
⑦ 用「ref 标志位」绕过 StrictMode 的双调用(掩盖真问题,见 8.5)
⑧ 在 commit 阶段做重计算(commit 不可中断 → 直接阻塞绘制)
⑨ 认为「时间切片是每帧固定 5ms 执行一次」(它是动态判断的)
⑩ 把「并发渲染」理解成「多线程」(仍是单线程)
⑪ 认为「长列表用 transition 就不卡了」(总工作量不变,最终仍需虚拟滚动)
⑫ 忽略「childLanes 让 React 跳过无更新子树」这一优化

十、面试延伸

  1. 「Fiber 解决了什么问题?」
    它解决了「同步递归渲染无法中断」的问题。React 15 的 Stack Reconciler 用递归遍历整棵树,一旦开始就跑完——大列表/深树会阻塞主线程几百毫秒(用户输入无响应、动画掉帧)。Fiber 把渲染拆成「一个个工作单元」,用「链表指针(return/child/sibling)替代递归」,因此可以「暂停 / 恢复 / 丢弃」。这为「时间切片」「优先级调度」「并发渲染」提供了基础。

  2. 「为什么用链表遍历替代递归?」
    因为递归的调用栈无法「暂停与恢复」——JS 一旦进入递归就只能一路执行到底(除非抛异常)。而「链表遍历 = 一个循环 + 当前节点指针」:每处理完一个单元都可以「保存 nextUnitOfWork 并 return(让出主线程)」,下一帧再从该指针继续。这是「可中断渲染」的必要前提,也是 Fiber 采用「链式结构」的核心设计目的。

  3. 「什么是双缓存树?」
    React 在内存中维护两棵 Fiber 树:current(当前屏幕上显示的)与 workInProgress(正在构建的)。渲染时基于 current 创建/复用 workInProgress 的节点,两个节点通过 alternate 互相指向;工作完成后,只需把 root.current 指向新的树(一次指针切换)就完成了「提交」。好处:① 用户永远看到一致的一棵树;② 渲染中途被打断时可以直接丢弃 workInProgress 而不影响屏幕。

  4. 「render 阶段和 commit 阶段分别做什么?为什么 render 必须纯?」
    render 阶段:遍历 Fiber 树、执行组件函数、diff 出变化、收集副作用标记——可中断、可丢弃、可重放;commit 阶段:把副作用一次性应用到 DOM(before mutation → mutation → layout),并触发 effect,不可中断。render 必须纯的原因正是「它可能被中断后重放或整体丢弃」——如果里面有副作用(请求、改外部变量、随机值),就会重复执行或产生不一致的结果。

  5. 「时间切片是怎么实现的?」
    React 用独立的 Scheduler 调度:给一次工作分配一个时间片(约 5ms),每处理完一个 Fiber 单元就检查「是否用完时间片 / 是否有更高优先级任务」;如果是就让出主线程(用 MessageChannel 安排下一次继续),让浏览器处理输入/动画/绘制,下一帧再继续。两个关键点:① 被中断的是 render 阶段(commit 永远一次做完);② 只有「标记为非紧急的更新」(transition 优先级)才真正受益——click 这类 Sync 优先级的更新仍然同步执行。

  6. 「Lane 模型是什么?为什么要用位掩码?」
    Lane 是 React 18 的优先级模型——用「一个 32 位整数的不同位」表示不同优先级(SyncLane、InputContinuousLane、DefaultLane、TransitionLanes、IdleLane)。用位掩码的原因:① 一个 fiber 上可能同时有多个待处理更新,「按位或」就能把它们合并进一个整数;② 判断「是否包含某类优先级」只需位运算(极快);③ 能表达「一批车道」的概念。此外 childLanes 让 React 能「跳过整棵没有更新的子树」。

  7. 「Fiber 让 React 变快了吗?」
    不准确。Fiber 的主要收益是「响应性」而非「吞吐量」——它让长渲染不阻塞用户输入与动画(可中断),但单次渲染因为要维护 Fiber 结构与调度,总开销可能略增。所以更准确的说法是「Fiber 让 React 在长时间渲染时仍然可交互」,并且为优先级调度、Suspense、并发特性提供了基础。「响应性」与「吞吐量」是两个不同的指标,Fiber 优化的是前者。

  8. 「StrictMode 的双调用和 Fiber 有什么关系?」
    关系在「render 可以被执行多次」这个前提上。Fiber/并发架构允许「渲染被中断后重放」或「整体丢弃」——这意味着「组件函数可能被多次执行」。所以「render 必须是纯函数」从「最佳实践」升级为「正确性要求」。StrictMode 的双调用就是「在开发环境故意制造这个场景」,用来暴露「不纯的渲染」(如「在渲染里改外部变量」)与「缺失的清理」(如「订阅没写完清理函数」)。

一句话速记

Fiber 把「不可中断的递归」改成「可中断的链表遍历」(return/child/sibling 指针 + 循环 + nextUnitOfWork),因此能暂停/恢复/丢弃;双缓存树(current 与 workInProgress,用 alternate 互指)让「提交」只是一次指针切换,且被打断时可安全丢弃;渲染分两阶段——render(可中断、必须纯)与 commit(不可中断,mutation → layout,之后绘制,再异步 useEffect);时间切片约 5ms(Scheduler 调度,MessageChannel 续跑,只有 transition 优先级才真正受益);Lane 位掩码表达优先级(Sync → InputContinuous → Default → Transition → Idle),childLanes 让 React 跳过无更新的子树;Fiber 提升的是「响应性」而不是「吞吐量」;「render 可被多次执行」正是「StrictMode 双调用」与「render 必须纯」的共同根源。

赞(0) 打赏
未经允许不得转载:陌上寒 » 12.11 Fiber 解决了什么

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫