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 团队公开设计文档
相关推荐
Ticnix13 分钟前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人16 分钟前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl20 分钟前
OpenClaw 深度技术解析
前端
崔庆才丨静觅23 分钟前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人31 分钟前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼35 分钟前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空38 分钟前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust
Mr Xu_43 分钟前
Vue 3 中计算属性的最佳实践:提升可读性、可维护性与性能
前端·javascript
jerrywus1 小时前
我写了个 Claude Code Skill,再也不用手动切图传 COS 了
前端·agent·claude
玖月晴空1 小时前
探索关于Spec 和Skills 的一些实战运用-Kiro篇
前端·aigc·代码规范