普通视图

发现新文章,点击刷新页面。
今天 — 2025年10月17日首页

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

作者 DoraBigHead
2025年10月16日 18:00

参考:《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 架构重生记:从递归地狱到时间切片

作者 DoraBigHead
2025年10月14日 19:55

本文参考卡颂老师的《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 团队公开设计文档
❌
❌