阅读视图

发现新文章,点击刷新页面。

React Fiber:从“递归地狱”到“时间切片”的重生之路

前情回顾:
在上一篇中,我们聊到 React 的理念——“快速响应”。
可 React15 的老架构在深层组件更新时,却经常卡到让人想拔网线。
原因就是它那套不能中断的递归调和机制
一旦开始递归,就像打开了 Photoshop 的无限滤镜叠加,停不下来。

于是,React 团队痛定思痛,决定造一套能“中断更新”的新架构。
这,就是 Fiber 登场的时刻。


🧠 一、Fiber 是什么?为什么它能解决卡顿?

Fiber 的中文叫 纤程(Fiber) ,听上去像健身食品,其实是计算机里的一种概念。
它和进程(Process)、线程(Thread)、协程(Coroutine)一样,都是程序执行的过程单位

在很多文章中,会把“纤程”看作“协程”的一种实现。
而在 JavaScript 世界里,协程的实现方式就是——Generator 函数

也就是说,Fiber = React 内部的协程机制。

它让 React 更新变得可以:

  • 暂停(yield)
  • 恢复(resume)
  • 并根据优先级切片执行(priority scheduling)

如果用一句话总结 Fiber 的思想:

Fiber = React 内部实现的一套状态更新机制,支持任务的中断、恢复和复用中间状态。


🏗️ 二、Fiber 的诞生动机:递归的噩梦

让我们先回忆一下 React15 是怎么更新视图的。
它的调和器是递归实现的:

function updateComponent(component) {
  component.render();
  component.children.forEach(updateComponent);
}

听起来没毛病,对吧?
但问题在于:递归是同步执行的
假如组件树很深,或者子组件太多,主线程就会被 React 独占。

于是出现了这种尴尬场景👇

我输入一个字母 → 页面两秒没反应 → 再输入的时候,浏览器直接假死。

这时,React 团队意识到:
“不能再让递归绑架线程了,我们得改造整套调和机制!”

于是他们重写了整个核心,用循环 + 可中断单元任务取代了递归。
那套循环任务系统,就是 Fiber 架构。


🧩 三、Fiber 架构三重含义

Fiber 既是架构、也是节点,更是一种“任务思维”的体现。
我们可以从三层理解它:

层级 含义 比喻
架构 React16 的调和机制,支持可中断更新 从“递归调用栈”转成“任务队列”
静态结构 描述组件类型、DOM 节点等信息 元数据
动态工作单元 存放本次更新要做的任务 工作线程

🧬 四、Fiber 节点结构源码揭秘

Fiber 的定义在源码里其实很清晰(节选自 ReactFiber.new.js):

function FiberNode(tag, pendingProps, key, mode) {
  // 静态结构
  this.tag = tag;            // 类型:FunctionComponent / HostComponent 等
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;     // 对应的真实DOM节点

  // 树关系
  this.return = null;        // 父节点
  this.child = null;         // 第一个子节点
  this.sibling = null;       // 右边兄弟节点

  // 动态工作单元
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;

  // 调度
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 双缓冲机制
  this.alternate = null;
}

每个 Fiber 节点就是 React 更新的最小单元。
它知道:

  • 自己是谁(tag、type)
  • 自己的家人是谁(return、child、sibling)
  • 自己当前在做什么(pendingProps、lanes)
  • 自己上一次的状态在哪(alternate)

是不是像一个会“记仇”的任务机器人?
每次更新,它都会记下自己上次干了啥,下次再干能不能快点。


🌲 五、Fiber 树的长相

来看这段简单的组件代码:

function App() {
  return (
    <div>
      i am
      <span>KaSong</span>
    </div>
  );
}

编译后,React 会为每个节点生成一个对应的 Fiber 节点。
它们的关系如下:

App (FunctionComponent)
 └── div (HostComponent)
       ├── "i am" (HostText)
       └── span (HostComponent)

可以想象成下面这张“Fiber 家谱图”👇


🖼️ Fiber 树结构图

exported_image.png

每个 Fiber 节点的关系通过这三个指针维护:

属性 含义
child 第一个子节点
sibling 兄弟节点
return 父节点(执行完要返回)

🤔 为什么父指针叫 return

别急,这不是 React 开发者命名癖好。
而是因为 Fiber 是工作单元(Work Unit)

执行完当前节点后,程序会“return”回父节点继续处理。
这就像函数调用栈,只不过现在是我们手动维护的“链表版调用栈”。


⚙️ 六、Fiber 的双缓冲机制

每个 Fiber 节点都有个 alternate 指针,用于指向上一次的版本。
这样 React 就维护了两棵树:

  • 一棵是正在屏幕上展示的 current 树
  • 另一棵是正在计算中的 workInProgress 树

当更新完成后,React 只需轻轻一换指针:

current = workInProgress;

整个应用就完成了一次“无感更新”。
这种机制被称为 双缓冲(Double Buffering) ,类似显卡渲染帧切换,流畅无比。


🔧 七、手写一个迷你版 Fiber 执行器

我们用极简代码感受一下 Fiber 的执行逻辑:

class FiberNode {
  constructor(tag, parent = null) {
    this.tag = tag;
    this.return = parent;
    this.child = null;
    this.sibling = null;
  }
}

// 构建 Fiber 树
const App = new FiberNode("App");
const Div = new FiberNode("div", App);
const Span = new FiberNode("span", Div);
App.child = Div;
Div.child = Span;

// 模拟 Fiber 循环
function performUnitOfWork(fiber) {
  console.log("Work on:", fiber.tag);
  if (fiber.child) return fiber.child;
  let next = fiber;
  while (next) {
    if (next.sibling) return next.sibling;
    next = next.return;
  }
  return null;
}

let nextUnitOfWork = App;
while (nextUnitOfWork) {
  nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}

输出:

Work on: App
Work on: div
Work on: span

这就是 Fiber 的执行本质:

它把原本“整棵树的递归更新”,拆成了“一个节点一个节点的任务循环”,
每次可以中断,浏览器空闲时再继续。

这也正是 React 能实现 时间切片(Time Slicing) 的底层基础。


🧭 八、小结与展望

项目 React15 React16(Fiber)
调和方式 递归栈 链表循环
可中断性 ❌ 不可中断 ✅ 可中断恢复
更新粒度 整棵树 单个节点(Fiber)
状态保存 调用栈 Fiber 节点内存
渲染机制 一次性 分阶段(优先级调度)

Fiber 的出现,让 React 从“函数式渲染库”进化为“可调度的 UI 引擎”。

九、参考资料

  • 卡颂:《React 技术揭秘》
  • Acdlite - React Fiber Architecture (2016)
  • Lin Clark - A Cartoon Intro to Fiber (React Conf 2017)
  • React 源码(v18.2)
  • React RFC: Time Slicing and Scheduling

🪄 下一章预告:
我们将深入 Fiber 的执行流程,看看它是如何从 “beginWork → completeWork” 一步步构建出界面的。
就像流水线造车一样,每个 Fiber 都要经过一段工序,最终被打磨成真实 DOM。

React 中的代数效应:从概念到 Fiber 架构的落地

参考:《React 技术揭秘》 by 卡颂

一、前言:React,不只是“快”

React 团队做架构升级,从来不是为了单纯的“更快”。
如果只是性能,他们完全可以优化 reconciliation 算法或者 diff 策略。

他们真正追求的,是**“控制时间”**——
让 UI 的更新可被中断、调度、恢复,就像一位懂分寸的画家,
知道什么时候该收笔,什么时候该补色。

这正是 React Fiber 想要实现的哲学。

而理解它的钥匙,藏在一个看似“学术味”的概念里:
👉 代数效应(Algebraic Effects)


二、代数效应:一个让函数更“有礼貌”的思想

简单来说,代数效应解决的是一个老大难问题:

当一个函数既要保持纯净逻辑,又要处理副作用时,该怎么办?

我们先看一个极简例子👇

function getTotalPicNum(user1, user2) {
  const picNum1 = getPicNum(user1);
  const picNum2 = getPicNum(user2);
  return picNum1 + picNum2;
}

逻辑简单到极致——加法而已。
但一旦 getPicNum 变成异步(比如去服务器查图片数),
整条函数调用链就被 async/await 感染了:

async function getTotalPicNum(user1, user2) {
  const picNum1 = await getPicNum(user1);
  const picNum2 = await getPicNum(user2);
  return picNum1 + picNum2;
}

于是,你的整个项目从同步世界坠入了“Promise 地狱”。
这就像一场小感冒引发了全公司的核酸检测。

代数效应的思路是这样的:

副作用由外部捕获和恢复,函数内部依然保持纯净。

为了说明,我们用一段虚构语法来模拟它的思想(不是 JS 代码,只是概念演示):

function getPicNum(name) {
  const picNum = perform name; // 执行副作用
  return picNum;
}

try {
  getTotalPicNum('kaSong', 'xiaoMing');
} handle (who) {
  switch (who) {
    case 'kaSong':
      resume with 230;
    case 'xiaoMing':
      resume with 122;
  }
}

这里的 perform 会触发外层的 handle
resume 再将结果带回中断点继续执行。

也就是说,函数逻辑和副作用的执行被“分离”了。
听起来是不是有点像 React 的 “render” 与 “commit” 阶段?


三、Fiber 登场:React 的代数效应工程实现

React Fiber 是 React 团队为了解决同步递归更新无法中断的问题而重写的协调器。

换句话说,它是 React 的一次“灵魂重构”。

Fiber 的核心目标是:

  • 支持任务分片与优先级调度
  • 允许任务中断与恢复
  • 恢复后能复用中间结果

或者更通俗点说:

以前 React 渲染是一口气吃完的火锅;
Fiber 让它可以夹一口肉,放下筷子接个电话,再回来继续吃。


🌿 Fiber 是什么?

Fiber(纤程)并不是 React 发明的词。
它早就出现在计算机领域中,与进程(Process)、线程(Thread)、协程(Coroutine)并列。

在 JavaScript 世界里,协程的实现是 Generator
所以我们可以说:

Fiber 是 React 在 JS 里,用链表结构模拟协程的一种实现。

简单理解:
Fiber 就是一种可中断、可恢复的执行模型。


四、Fiber 节点结构:链表串起的「可中断栈」

Fiber 架构中,每个 React Element 对应一个 Fiber 节点,
每个 Fiber 节点都是一个「工作单元」。

结构大致如下👇

FiberNode {
  type,          // 对应组件类型
  key,           // key
  return,        // 父 Fiber
  child,         // 第一个子 Fiber
  sibling,       // 兄弟 Fiber
  pendingProps,  // 即将更新的 props
  memoizedProps, // 已渲染的 props
  stateNode,     // 对应的 DOM 或 class 实例
}

它的核心是用链表结构,模拟函数调用栈
这样 React 就能“暂停栈帧”,在浏览器空闲时恢复执行。

想象一个任务循环(伪代码):

while (workInProgress && !shouldYield()) {
  workInProgress = performUnitOfWork(workInProgress);
}

shouldYield() 会检测浏览器时间是否用完。
如果主线程要去干别的事(比如动画、用户输入),React 会体贴地暂停。

这,就是 React Fiber 的“可中断渲染”精髓。


五、那 Generator 呢?为什么不用它?

有人会问:

“Generator 不也能暂停和恢复吗?为什么 React 要造轮子?”

确实,Generator 曾经是候选方案。
但它的问题在于两点:

  1. 传染性太强:用过 yield 的函数,整条调用链都得改。
  2. 上下文绑定太死:一旦被中断,就难以灵活恢复特定任务。

举个例子:

function* doWork(A, B, C) {
  var x = doExpensiveWorkA(A);
  yield;
  var y = x + doExpensiveWorkB(B);
  yield;
  var z = y + doExpensiveWorkC(C);
  return z;
}

如果任务中途被打断,或者来个更高优的任务插队,
xy 的计算状态就乱套了。

而 Fiber 则通过链表结构,把中间状态封装在节点上,
可以安全暂停、恢复、复用。


六、Fiber 调度:React 的“任务分片大师”

React Fiber 的渲染过程分两阶段:

  1. Render 阶段(可中断) :生成 Fiber 树,计算变更。
  2. Commit 阶段(不可中断) :执行 DOM 操作,提交更新。

示意图如下:

Render(可中断) ----> Commit(同步)
     ↑                    ↓
   调度器控制         应用更新

这正是代数效应的工程化体现:
逻辑阶段(Render)可以被中断和恢复,
副作用阶段(Commit)则由系统集中处理。


七、Hooks:代数效应的另一种体现

再看看 Hook:

function App() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

这里的 useStateuseReducer 等 Hook,
其实也是代数效应思想的体现:

组件逻辑中不关心状态保存的机制,只管声明“我需要一个状态”。

React 内部帮你处理 Fiber 节点中的状态链表。
你只需关心「逻辑」而非「调度」,
就像写同步代码一样,优雅得让人上瘾。


八、图解:Fiber = 可恢复的调用栈

App
 ├── Header
 │    └── Logo
 ├── Content
 │    ├── List
 │    └── Detail
 └── Footer

Fiber 的结构类似于:

  • child 指向第一个子节点;
  • sibling 指向兄弟节点;
  • return 指回父节点。

这让 React 能像遍历树一样遍历组件,
并随时暂停、恢复任务,而不丢上下文。


九、总结:React 的“时间魔法”

如果你把 React 比作魔术师,那 Fiber 就是它的魔杖。
它让 React 拥有了“操控时间”的能力:

  • 可以让任务暂停,等浏览器忙完再继续;
  • 可以按优先级执行任务(比如用户输入优先于动画);
  • 可以在恢复时复用中间状态,不浪费计算。

🎯 一句话总结:

React Fiber = 代数效应 + 调度器 + 状态复用


React 架构重生记:从递归地狱到时间切片

本文参考卡颂老师的《React 技术揭秘》,并结合小dora个人理解与源码阅读编写的一篇博客。
目标是让你看懂:React 为什么要重写架构、Fiber 到底解决了什么问题。


一、React15:一个“全力以赴但不会刹车”的系统

React15 的架构只有两层:

  • 🧩 Reconciler(协调器) :负责计算哪些组件要更新;
  • 🖼️ Renderer(渲染器) :把更新同步到对应平台(浏览器、原生、测试环境等)。

听起来没问题,但问题出在它的更新策略——
React15 在更新时使用的是递归调用

每次调用 setState() 时,React 会自上而下递归遍历整棵组件树。

我们可以用伪代码看看它的本质:

function updateComponent(component) {
  component.render(); // 渲染当前组件
  component.children.forEach(updateComponent); // 递归子组件
}

简单粗暴,效率直接。
但问题是——一旦递归开始,就停不下来


🧠 举个例子:

假设你有一棵很深的组件树,当用户点击按钮触发更新时,
React 就会一路递归更新下去:

App
 ├─ Header
 ├─ Main
 │   ├─ List
 │   │   ├─ Item #1
 │   │   ├─ Item #2
 │   │   └─ Item #3
 │   └─ Sidebar
 └─ Footer

当层级很深、每个组件都要执行 render() 时,
整个递归过程会持续超过 16ms(一帧的理想渲染时间)。

这意味着在更新的过程中,浏览器完全没有机会响应用户操作

想点击?等我更新完再说。
想输入?我还在 render 呢。

这,就是 React15 最大的痛点——同步更新不可中断


二、如果在中途强行“打断”会发生什么?

假设我们有个 Demo:

function List({ items }) {
  return (
    <ul>
      {items.map((num) => (
        <li key={num}>{num * 2}</li>
      ))}
    </ul>
  );
}

用户希望看到 [1, 2, 3] → [2, 4, 6]

如果中途在更新到第二个 <li> 时被中断,就可能出现半成品页面:

<li>2</li>
<li>2</li>
<li>3</li>

React15 没法处理这种情况。因为它没有保存中间状态,也没有“恢复机制”。
它只能一口气跑完。

这时候 React 团队意识到:

我们需要一个可以「暂停、恢复、甚至丢弃」任务的架构。


三、React16:Fiber——让 React 学会「调度」

于是,在 React16 中,React 团队重写了整个协调层,设计了新的架构:

+------------------+
|   Scheduler      | 调度器:分配优先级,安排执行顺序
+------------------+
|   Reconciler     | 协调器:找出变化的组件(Fiber)
+------------------+
|   Renderer       | 渲染器:将变化反映到宿主环境
+------------------+

新增的那一层 Scheduler(调度器) 就是关键!


🧬 Fiber 是什么?

简单来说,Fiber 是对「组件更新单元」的抽象
每个组件都会对应一个 Fiber 对象,它保存:

{
  type: Component,
  pendingProps: newProps,
  child: firstChildFiber,
  sibling: nextFiber,
  return: parentFiber
}

它就像是一个链表节点,连接整棵组件树。
通过 Fiber,React 可以记录任务执行的进度


🔁 可中断的循环

React16 的更新逻辑不再是递归,而是循环:

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

每次只处理一个 Fiber 单元,然后问一句:

if (shouldYield()) pause();

shouldYield() 就是核心判断:
👉 当前帧的时间是否用完?
👉 有没有更高优任务进来?

如果答案是“是”,就中断执行,把控制权交还给浏览器。

React 会在下一帧或空闲时间里继续从中断点恢复


四、Scheduler:React 的「时间管理大师」

Fiber 可以被打断,但谁来决定打断时机

这就轮到 Scheduler 登场了。

浏览器有个原生 API requestIdleCallback()
可以在浏览器空闲时执行任务,但它兼容性和触发频率都不稳定。

于是 React 自己实现了一个更强的版本:

📦 scheduler
它模拟浏览器空闲回调,并为任务赋予多种优先级。

每个任务都带有权重,比如:

优先级 说明 示例
Immediate 立即执行 错误边界恢复
UserBlocking 用户输入 输入框响应
Normal 常规更新 列表渲染
Low 低优任务 动画或日志
Idle 空闲任务 后台预加载

通过这种优先级机制,React 终于可以像操作系统一样分配 CPU 时间。


五、渲染:内存标记 + 批量提交

Fiber 负责协调,Renderer 才是执行者。
在 React16 中,Reconciler 不再边遍历边渲染,而是先打标记、后统一提交

比如:

export const Placement = 0b0000000000010;
export const Update = 0b0000000000100;
export const Deletion = 0b0000000001000;

每个 Fiber 节点在内存中被打上这些标签。
等所有标记完成后,Renderer 一次性提交所有 DOM 变更。

这就保证了即使中途被中断,DOM 始终保持一致性


六、可视化理解:React15 vs React16

对比项 React15 React16 (Fiber)
架构层次 Reconciler + Renderer Scheduler + Reconciler + Renderer
更新机制 递归 循环
可中断性 ❌ 不可中断 ✅ 可中断
DOM 一致性 更新中可能闪烁 内存标记后统一提交
优先级调度 有(Scheduler)
源码模块 ReactDOM react-reconciler + scheduler

📊 可以把这两者比喻成:

  • React15:单线程跑完一场马拉松,中途谁也拦不住;
  • React16:多任务分片执行,随时暂停、恢复、插队。

七、总结:从渲染引擎到时间调度系统

React16 的架构重写并非简单的性能优化,
而是一种“调度哲学的引入”。

React 不再只是「渲染 DOM 的库」,
而是一个「管理任务优先级的调度系统」。

Fiber 让任务可中断;
Scheduler 让任务有先后;
Renderer 让任务有结果。

React 的底层逻辑已经从:

同步执行异步调度

演化成一套“以用户体验为核心的调度架构”。


📘 参考资料

  • 卡颂,《React 技术揭秘》
  • React 官方源码(react-reconciler / scheduler)
  • React 团队公开设计文档
❌