Skip to content

React:把界面重新计算一遍

React 最重要的发明,不是 JSX,也不是 Virtual DOM。它真正改变行业的地方,是让开发者敢于把 UI 当成一个可以反复执行的计算:给定同样的输入,就重新描述一次想要的界面;至于如何把两次描述之间的差异安全、及时地落实到不同宿主环境,由运行时承担。

今天回看,这个想法显得近乎自然。2013 年却不是如此。当时主流前端架构倾向于把数据对象、DOM 节点和一组双向绑定连接起来,更新意味着沿着依赖图传播变化。React 反过来提议:不要精确维护每一条绑定;让组件函数再执行一次,然后比较两次结果。

这部 Chronicle 追踪的不是版本号,而是这个提议被不断放大的过程:

  1. 为了让“全部重算”在浏览器里成立,React 建立了 element、reconciliation、transaction 和事件插件系统。
  2. 为了让同一套组件模型离开 DOM,它把核心与 renderer 分开,并由此孕育 React Native。
  3. 为了让重算不再长时间霸占主线程,它把隐含在 JavaScript 调用栈里的工作改造成 Fiber 数据结构。
  4. 为了让状态逻辑摆脱 class 实例,它把 Hook 记录挂到 Fiber 上,并用调用顺序建立身份。
  5. 为了让等待、优先级和流式交付进入组件模型,它把并发渲染、Suspense、Fizz、Flight 和 Actions 逐层接入。
  6. 为了减少开发者手工维护引用稳定性的负担,它最终又把一部分运行时优化前移到 React Compiler。

React 架构演化总览

这不是一部“React 功能大全”

本文选择那些改变了后续决策空间的节点。某个版本即使新增了大量 API,只要没有改变 React 对“组件、工作、宿主、时间或网络边界”的理解,就不会得到同等篇幅。相反,一个早期 PR 即使当时不能运行真实应用,只要它第一次把未来架构的关键约束写进代码,就可能占据整节。

每章使用三类证据标签:

史料事实:可以从提交、PR、RFC、发布说明或同期官方文章直接确认。

参与者回忆:来自项目创建者或维护者的事后叙述。它有一手价值,但也可能受到记忆和后见之明影响。

本文判断:基于多份材料做出的工程解释。它不是项目方原话,读者可以不同意。

完整资料索引见资料与注释,研究过程见仓库中的 research/react/

章节地图

章节时间焦点要回答的问题
第一章2010–2012Bolt、XHP、FaxJS 和函数式语言如何汇合成 React 的前身?
第二章2013–2014为什么 React 故意把标记、逻辑和组件放回 JavaScript?
第三章2013–2015element、key、事务与合成事件怎样支撑“重新 render”?
第四章2014–2016React 拒绝成为完整框架,如何同时催生创新和碎片化?
第五章2013–2016从 iOS 原型到 React Native,DOM 如何变成一个可替换宿主?
第六章2015–2017为什么旧 reconciler 不能靠局部优化获得可中断渲染?
第七章2017–2020如何在重写内核后仍让绝大多数应用平滑升级?
第八章2018–2019Hook 为什么依赖调用顺序,它解决和制造了什么问题?
第九章2018–2022“Concurrent Mode”为何被拆成渐进启用的一组 feature?
第十章2020–2026SSR、RSC、Flight 与 Actions 到底分别跨越了什么边界?
第十一章2017–至今Compiler、框架推荐与独立基金会揭示了怎样的新 React?

六条贯穿全书的因果链

1. 从精确更新,转向可重复计算

React 没有消灭更新成本,只是改变了成本的归属。开发者不再手写大量“当 A 变化时更新 B”的指令;运行时承担比较、批处理和宿主提交。这个交换只有在 render 可重复、描述对象足够轻、DOM 改动足够少时才成立。

2. 从 DOM 库,转向 renderer 协议

早期 React 已经有服务端和原生 iOS 原型,但直到 React Native 与 react/react-dom 拆包,这个事实才成为公开架构:组件计算与最终输出到哪里并不是同一件事。Fiber 后来的 Host Config 正是这条边界的制度化。

3. 从同步递归,转向显式工作单元

旧 reconciler 让 JavaScript 调用栈替 React 保存“做到哪了”。这很简单,却无法安全暂停。Fiber 把当前位置、子节点、兄弟节点、优先级和副作用都显式存入对象,使“先做一点、让出主线程、稍后继续或丢弃”成为可能。

4. 从组件实例,转向渲染期间的状态记录

class 把状态身份绑定到实例,Hooks 则把状态记录按调用顺序挂在 Fiber 上。它减少了逻辑复用所需的包装层,也把一套新的约束——纯渲染、稳定调用顺序、闭包快照、依赖数组——带入日常编程。

5. 从页面首次输出,转向持续流动的 UI

传统 SSR 的目标是尽快得到一串 HTML。Suspense、Fizz 与选择性 hydration 开始把页面当成可分段完成的工作;Flight 又把服务器组件结果编码为可增量传输的组件模型。网络、数据依赖和渲染调度从此不再是 React 外部的纯应用问题。

6. 从人工优化,转向编译分析

PureComponentmemouseMemouseCallback 都要求开发者理解引用身份。React Compiler 试图通过控制流、数据流和可变性分析自动划分 reactive scope。它没有替代 reconciler,而是把“哪些值可以复用”的部分判断从人移交给构建时工具。

Code Time Machine:建议按这个顺序读源码

快照入口观察重点
2013 初始公开版ReactCompositeComponent.jsrender 已被要求无副作用;class、mixin 与生命周期协议同时存在。
2013 初始公开版ReactReconcileTransaction.jsDOM 更新、事件开关和 mount-ready 回调如何被事务包装。
2013 初始公开版EventPluginHub.js事件提取、排队和派发为什么被设计成插件管线。
2016 Fiber 起步PR #6690Noop renderer、nextUnitOfWork 与按 deadline 让出执行权。
2016 双缓冲树PR #6981alternate、current/work-in-progress 两棵树和对象复用。
2016 提交阶段PR #7154先完成整棵树的协调,再沿副作用链调用宿主环境。
2019 HooksReactFiberHooks.js@v16.8.0Hook 链表、dispatcher、调用顺序校验和 render-phase update。
2022 LanesReactFiberLane.new.js@v18.2.0多种工作如何编码到位掩码,并选择下一批 lanes。
2024–2026 FlightReactFlightServer.js@v19.2.0Server/Client reference、chunk、thenable 与流式协议的交汇。
2025 CompilerHIR.ts@v19.2.0AST 如何降低为 CFG/HIR,再变成 reactive scopes 并回写 AST。

阅读这些快照时,不要先问“函数最终怎么跑完”,而要问四个问题:状态存在哪里?工作如何表示?什么时候允许产生副作用?哪个边界由宿主或框架负责?React 的每次重大演化,几乎都在重新回答其中至少一个。

图解入口

阅读路线

第一次系统理解 React,可按章节顺序阅读。已经熟悉日常 API、想研究实现,可先读第三、六、八、九、十章,再回到一、二章理解这些约束为何出现。正在做 bundler、renderer 或全栈框架,建议重点关注第五、九、十、十一章:React 对外看似是组件 API,对集成者暴露的却是一组不断扩张的调度、模块引用、流协议和编译契约。

下一章从 React 还不叫 React 的时候开始:不是从某个天才瞬间开始,而是从一套已经难以维护的广告界面系统开始。

Engineering history, reconstructed from primary sources.