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

本文参考卡颂老师的《React 技术揭秘》,并结合小dora个人理解与源码阅读编写的一篇博客。

目标是让你看懂:React 为什么要重写架构、Fiber 到底解决了什么问题。


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

React15 的架构只有两层:

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

听起来没问题,但问题出在它的更新策略 ------

React15 在更新时使用的是递归调用

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

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

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

简单粗暴,效率直接。

但问题是------一旦递归开始,就停不下来


🧠 举个例子:

假设你有一棵很深的组件树,当用户点击按钮触发更新时,

React 就会一路递归更新下去:

less 复制代码
App
 ├─ Header
 ├─ Main
 │   ├─ List
 │   │   ├─ Item #1
 │   │   ├─ Item #2
 │   │   └─ Item #3
 │   └─ Sidebar
 └─ Footer

当层级很深、每个组件都要执行 render() 时,

整个递归过程会持续超过 16ms(一帧的理想渲染时间)。

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

想点击?等我更新完再说。

想输入?我还在 render 呢。

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


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

假设我们有个 Demo:

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

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

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

css 复制代码
<li>2</li>
<li>2</li>
<li>3</li>

React15 没法处理这种情况。因为它没有保存中间状态,也没有"恢复机制"。

它只能一口气跑完。

这时候 React 团队意识到:

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


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

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

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

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


🧬 Fiber 是什么?

简单来说,Fiber 是对「组件更新单元」的抽象

每个组件都会对应一个 Fiber 对象,它保存:

yaml 复制代码
{
  type: Component,
  pendingProps: newProps,
  child: firstChildFiber,
  sibling: nextFiber,
  return: parentFiber
}

它就像是一个链表节点,连接整棵组件树。

通过 Fiber,React 可以记录任务执行的进度


🔁 可中断的循环

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

scss 复制代码
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

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

scss 复制代码
if (shouldYield()) pause();

shouldYield() 就是核心判断:

👉 当前帧的时间是否用完?

👉 有没有更高优任务进来?

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

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


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

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

这就轮到 Scheduler 登场了。

浏览器有个原生 API requestIdleCallback()

可以在浏览器空闲时执行任务,但它兼容性和触发频率都不稳定。

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

📦 scheduler

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

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

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

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


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

Fiber 负责协调,Renderer 才是执行者。

在 React16 中,Reconciler 不再边遍历边渲染,而是先打标记、后统一提交

比如:

ini 复制代码
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 团队公开设计文档
相关推荐
彩旗工作室5 小时前
WordPress 本地开发环境完全指南:从零开始理解 Local by Flywhee
前端·wordpress·网站
iuuia6 小时前
02--CSS基础
前端·css
kyle~6 小时前
Qt---setAttribute设置控件或窗口的内部属性
服务器·前端·c++·qt
FIN66686 小时前
昂瑞微:射频与模拟芯片领域的国产领军者
前端·人工智能·科技·前端框架·智能
苦夏木禾6 小时前
css实现表格中最后一列固定
前端·javascript·css
LuHang6 小时前
WebSocket服务封装实践:从连接管理到业务功能集成
前端·websocket
九十一6 小时前
vue2中$set的原理
前端
闲不住的李先森6 小时前
深入解析 Cursor 规则:为团队打造统一的 AI 编程规范
前端·ai编程·cursor