普通视图
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 曾经是候选方案。
但它的问题在于两点:
-
传染性太强:用过
yield
的函数,整条调用链都得改。 - 上下文绑定太死:一旦被中断,就难以灵活恢复特定任务。
举个例子:
function* doWork(A, B, C) {
var x = doExpensiveWorkA(A);
yield;
var y = x + doExpensiveWorkB(B);
yield;
var z = y + doExpensiveWorkC(C);
return z;
}
如果任务中途被打断,或者来个更高优的任务插队,
那 x
和 y
的计算状态就乱套了。
而 Fiber 则通过链表结构,把中间状态封装在节点上,
可以安全暂停、恢复、复用。
六、Fiber 调度:React 的“任务分片大师”
React Fiber 的渲染过程分两阶段:
- Render 阶段(可中断) :生成 Fiber 树,计算变更。
- Commit 阶段(不可中断) :执行 DOM 操作,提交更新。
示意图如下:
Render(可中断) ----> Commit(同步)
↑ ↓
调度器控制 应用更新
这正是代数效应的工程化体现:
逻辑阶段(Render)可以被中断和恢复,
副作用阶段(Commit)则由系统集中处理。
七、Hooks:代数效应的另一种体现
再看看 Hook:
function App() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
这里的 useState
、useReducer
等 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 团队公开设计文档