React Fiber 的意义

59 阅读9分钟

🎯 React Fiber 的出现

React16 之前的渲染模式是不可中断的,如果目前执行的任务过长,就会出现以下问题:

  • 用户点按钮半天没反应
  • 动画卡得像 PPT
  • 页面像被人按下了暂停键

为了解决 React 渲染不友好的问题,让 React 变得更灵活(被打断也可以继续),所以需要一个全新的运行时架构

  • 可切片
  • 可暂停
  • 可恢复
  • 可优先级调度

🧠 React Fiber 的定义

Fiber 是可中断的渲染引擎,是把组件更新变成“可暂停的工作单元”的调度系统。

更具体的理解就是:

  • Fiber 是一种链表结构的 vnode
  • 它把每一个组件渲染拆成任务单元(unit of work)
  • 所有任务都会被调度器按照优先级先后处理

🧩 React Fiber 的关键能力

💪 1. 可中断渲染(Time Slicing)

之前现在
渲染不可中断,会阻塞其他事件,如用户点击等渲染可中断,在任务执行过程中随时会给浏览器让步,等高优任务结束后再恢复执行

🎚️ 2. 优先级调度(Scheduler)

React Fiber 会给每个更新都贴上“优先级标签”,像这样:

更新类型优先级(lane)
用户点击🟥 超高优先级
输入框输入🟧 很高
普通渲染🟩 中等
transition🟦 低一点
日志、统计🟪 淡淡的随缘优先级

用户点击了按钮 => 触发最高lane 任务 异步数据回来 => 触发中等lane任务 transition动效更新 => 触发更低lane任务

React 会根据这些任务的 lane 来决定当前应该执行哪个任务,以及后续任务的执行顺序。

🐱 3. 可恢复渲染(Pause & Resume)

React Fiber 中的每个节点都是一个 Fiber 对象,每次渲染一个 Fiber,都会在调度器中记录一个TODO List

Fiber A:处理中...
Fiber B:处理中...
Fiber C:处理中...
--给浏览器让步--
Fiber D:继续处理...

🔁 4. 双缓冲结构(alternate)

每个 Fiber 对象都有一个 alternate指针,分别指向两棵树:

  • current:现在正在屏幕上的版本
  • workInProgress:正在构建的新版本

渲染期间 React 在 workInProgress 上完成所有的 diff 对比及更新操作,等渲染完毕后再将所有更改全部更新到current上面。

🧱 React Fiber 的执行流程

整个更新流程参考图如下所示:

🐱 1. 触发更新(setState / dispatch)  
       ↓
🐱 2. 调度(分配 lane优先级)  
       ↓
🐱 3. render 阶段(可中断)
      - beginWork(创建新 fiber)
      - completeWork(收集副作用 flags)
       ↓
🐱 4. commit 阶段(不可中断)
      - mutation:动 DOM
      - layout:执行 layout effect
      - passive:执行普通 effect

render 可以被打断,commit 绝对不会被打断

  • Render 阶段 = 列购物清单(只写要买什么,不真买)
  • Commit 阶段 = 拿着清单去超市(真花钱、真拿货)

📝 Render 阶段:列清单

React 遍历组件树,找出「哪些地方需要更新」,但不动真实 DOM

// 你的代码
function App() {
  const [count, setCount] = useState(0);
  return <div>{count}</div>;
}

// Render 阶段做的事:
// 1. 发现 count 从 0 变成 1
// 2. 算出 div 的内容要改成 "1"
// 3. 记下来:{ 操作: '更新文本', 目标: div, 新内容: '1' }
// 4. 继续看下一个组件...

① 可以中断

// 就像写清单时,可以接个电话再回来继续写
while (有任务 && 没超时) {
  处理一个节点();
}

if (还有任务) {
  下次空闲继续();  // 不阻塞用户操作
}

② 走两遍(DFS)

向下走(beginWork):看这个组件要不要更新
向上走(completeWork):把子组件的结果汇总给父组件

Render 阶段遍历 workInProgress 树,进行 diff 计算,把发现的需要改动的地方记在副作用链表上。

Render 阶段开始
     ↓
遍历 Fiber 树
     ↓
发现 A 需要更新 → 把 A 加入链表:A → null
     ↓
发现 B 需要更新 → 把 B 接在后面:AB → null
     ↓
发现 C 需要更新 → 把 C 接在后面:AB → C → null
     ↓
遍历结束
     ↓
【产物】root.firstEffect = AB → C → null
     
workInProgress 树(完整结构)
├── div
│   ├── p (不变) 
│   └── span (变了) ← 标记 effectTag='UPDATE'
└── footer (不变)

            ↓
            
副作用链表(只记变化)
UPDATE → null
 ↑
这个 span 节点(引用自 workInProgress 树)

            ↓
            
Commit 阶段
拿到链表 → 找到 span 节点 → 看 workInProgress 树知道它该变成什么样 → 更新 DOM

🔧 Commit 阶段:去超市

Commit 阶段操作的是 workInProgress 树对应的 DOM,操作完成后交换指针,让 workInProgress 变成新的 current,整个过程就像「拿着新衣服穿上,然后标签改成新衣服」。

function commitRoot(root) {
  // 1. 操作 DOM(基于 workInProgress)
  commitMutationEffects(root.finishedWork);
  
  // 2. DOM 已经更新完成
  
  // 3. ⚡️ 交换指针(一瞬间)
  root.current = root.finishedWork;  // workInProgress 变成新的 current
  
  // 4. 现在状态一致了
  // current = 新树 (props 和 DOM 都对了)
  // workInProgress = 旧树 (下次备用)
  
  // 5. 执行生命周期
  commitLayoutEffects(root.current);
}

三大步骤

// 1. before mutation(动手前)
执行 getSnapshotBeforeUpdate  // 比如:记录滚动位置

// 2. mutation(动手!)
真正操作 DOM  // 插入、删除、更新属性

// 3. layout(动手后)
执行 componentDidMount/Update  // 比如:读取 DOM 位置
执行 useEffect

🎭 还是装修的例子

场景Render 阶段Commit 阶段
刷墙量尺寸、算材料、画图纸搬家具、刷漆、收拾
做饭想菜谱、列食材、备菜开火炒、装盘、上桌
网购逛淘宝、加购物车下单付款、收货

关键:Render 阶段可以「逛累了歇会儿」,Commit 阶段必须「付款一气呵成」!

💡 为什么这么设计?

1. 不卡顿

// 如果没有两个阶段
一次性计算+更新 10000 个节点 → 页面卡死 200ms

// 有了两个阶段
Render: 分 5 次计算,每次 5ms,中间让用户点鼠标 ✅
Commit: 一次性更新,只卡 20ms,用户感觉不到

2. 保证一致性

// Commit 必须一次性
如果更新一半被打断 → 页面显示一半新一半旧 → 错乱 ❌

3. 优先级调度

用户点击按钮(高优先级)→ 打断数据加载(低优先级)→ 先处理点击
对比项Render 阶段Commit 阶段
通俗理解列购物清单去超市买单
操作对象虚拟 DOM(Fiber 树)真实 DOM
能否中断✅ 能❌ 不能
主要工作找不同、列计划执行计划
相关生命周期render, shouldUpdateDidMount, DidUpdate
开发体验感觉不到感觉 DOM 变了

🎯 双缓冲结构

双缓冲就是 React 在内存里同时维护两棵 Fiber 树,一棵展示当前界面(current),一棵构建下一个界面(workInProgress),就像PPT的「当前页」和「编辑页」。

🌲 两棵树:current 和 workInProgress

// React 内存中同时存在两棵 Fiber 树
const fiberTree = {
  current: '正在屏幕上显示的树',     // 用户当前看到的界面
  workInProgress: '正在后台构建的树'  // 下一个要展示的界面
};
场景current 树workInProgress 树
PPT 演示大屏幕上正在放的页面你在电脑上偷偷改的下一页
装修房子你正在住的房子隔壁正在装修的新房
换衣服身上穿的衣服手里拿着的要换的衣服

Render 阶段:构建 workInProgress 树

function renderPhase(root) {
  // 1. 拿到当前显示的树
  const currentTree = root.current;
  
  // 2. 创建或复用 workInProgress 树
  let workInProgress = currentTree.alternate;
  if (!workInProgress) {
    // 第一次渲染,创建新树
    workInProgress = createFiberFrom(currentTree);
    currentTree.alternate = workInProgress;
    workInProgress.alternate = currentTree;
  }
  
  // 3. 在 workInProgress 树上进行 Render 阶段工作
  performWork(workInProgress);
  
  // 4. Render 结束,workInProgress 树准备好了
  // 但它还在内存里,没显示到屏幕
}

关键:用户在屏幕上看到的还是 current 树,完全不受影响!

🧭 alternate 指针

// 每个 Fiber 节点都有一个 alternate 指向对方的对应节点
const fiberNode = {
  stateNode: <div>,        // 真实 DOM
  child: ...,              // 子节点
  sibling: ...,            // 兄弟节点
  
  // 关键:指向另一棵树上对应的节点
  alternate: {
    // 这是另一棵树上的同一个组件
    stateNode: <div>,      // 指向同一个 DOM!
    child: ...,
    sibling: ...,
    alternate: fiberNode   // 互相指向
  }
};

两棵树共享同一个真实 DOM 节点,只是状态不同!

Commit 阶段:交换指针

function commitRoot(root) {
  // 1. 拿到已经构建好的 workInProgress 树
  const finishedWork = root.workInProgress;
  
  // 2. 执行所有 DOM 操作(mutation 阶段)
  commitMutationEffects(finishedWork);
  
  // 3. ⚡️ 关键步骤:交换指针!
  // 让 workInProgress 树变成新的 current 树
  root.current = finishedWork;
  
  // 4. 旧的 current 树变成下一次的 workInProgress
  // 下次 Render 时可以直接复用
}

指针交换的瞬间

// 交换前
root.current = 旧树 (正在显示)
root.workInProgress = 新树 (构建完成,还没显示)

// 执行 DOM 更新(用户看到界面变了)

// 交换指针(一瞬间完成)
root.current = 新树 (现在显示这个)
root.workInProgress = 旧树 (变成下一次的草稿)

就像:PPT 点击「下一页」的那一瞬间,新页面出现,旧页面变成草稿

💡 为什么需要双缓冲?

  1. 防止页面闪烁
// 如果没有双缓冲
function update() {
  删除旧DOM();      // 用户看到界面没了 ❌
  创建新DOM();      // 白屏一下再出现
}

// 有双缓冲
function update() {
  在内存构建新树();   // 用户啥也看不见
  一次性替换整个树();  // 瞬间切换,没有中间状态 ✅
}
  1. 复用节点,节省内存
// 两棵树共享 DOM 节点
旧树.节点A.alternate = 新树.节点A  // 指向同一个 <div>

// 如果节点没变化
新树.节点A = 旧树.节点A  // 直接复用,不用新建
  1. 支持中断恢复
// Render 阶段可以在 workInProgress 树上中断
function renderWithTimeSlice() {
  while (nextUnitOfWork && 没超时) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }
  
  if (nextUnitOfWork) {
    // 中断了,但没关系
    // workInProgress 树只构建了一半
    // current 树还在正常显示
    requestIdleCallback(继续构建);
  } else {
    // 构建完成,可以切换了
    commitRoot();
  }
}

📊 两棵树的生命周期

阶段current 树workInProgress 树
初始渲染null正在构建
首次渲染后树A (显示)树A (也指向相同)
第一次更新树A (显示)树B (构建中)
更新完成树B (显示)树A (备用)
第二次更新树B (显示)树A (构建中)
更新完成树A (显示)树B (备用)

两棵树轮流当「展示树」和「构建树」,交替使用!


1. 为什么叫「双缓冲」?
   - 源自图形学:两个缓冲区交替显示和绘制
   - 防止屏幕撕裂、闪烁

2. 什么时候用到了双缓冲?
   - Render 阶段:在 workInProgress 上工作
   - Commit 阶段:交换 current 和 workInProgress

3. 性能收益?
   - 用户无感知的更新
   - 可中断渲染
   - 节点复用

🎚️ 调度系统(Scheduler)

React 其内部会周期性调用 shouldYield() 询问浏览器是否需要让步,如果浏览器需要渲染动画、响应用户操作,React 会暂停自己的渲染工作。

如果让步:

pause → 等下再继续 → resume

🏎 lane 模型

在 Fiber 内部,每个更新会被标记成 lane:

00001 → 高优先级更新
00010 → 普通更新
00100transition
01000 → 空闲任务
…

比如来了一个点击事件(超高优先级):

pendingLanes = 00001

React 检查当前渲染是否比它慢:

  • 若慢,立即 中断当前渲染
  • 处理高优先级任务
  • 再回来继续做之前没做完的

🎤 总结

  1. “Fiber 就是一个可以被切片、暂停、恢复的渲染引擎,它把每个组件拆成可调度的工作单元,并用 lane 模型管理优先级,让 React 能在高负载下保持流畅的用户交互。”
  2. “render 阶段可中断,commit 阶段不可中断,这是保证用户看到 UI 一致性的关键。”
  3. “双缓冲结构让 React 可以在后台构建新的 UI,再一次性替换出来。”

“时间切片让渲染不再阻塞用户输入。”