React:把界面重新计算一遍
React 最重要的发明,不是 JSX,也不是 Virtual DOM。它真正改变行业的地方,是让开发者敢于把 UI 当成一个可以反复执行的计算:给定同样的输入,就重新描述一次想要的界面;至于如何把两次描述之间的差异安全、及时地落实到不同宿主环境,由运行时承担。
今天回看,这个想法显得近乎自然。2013 年却不是如此。当时主流前端架构倾向于把数据对象、DOM 节点和一组双向绑定连接起来,更新意味着沿着依赖图传播变化。React 反过来提议:不要精确维护每一条绑定;让组件函数再执行一次,然后比较两次结果。
这部 Chronicle 追踪的不是版本号,而是这个提议被不断放大的过程:
- 为了让“全部重算”在浏览器里成立,React 建立了 element、reconciliation、transaction 和事件插件系统。
- 为了让同一套组件模型离开 DOM,它把核心与 renderer 分开,并由此孕育 React Native。
- 为了让重算不再长时间霸占主线程,它把隐含在 JavaScript 调用栈里的工作改造成 Fiber 数据结构。
- 为了让状态逻辑摆脱 class 实例,它把 Hook 记录挂到 Fiber 上,并用调用顺序建立身份。
- 为了让等待、优先级和流式交付进入组件模型,它把并发渲染、Suspense、Fizz、Flight 和 Actions 逐层接入。
- 为了减少开发者手工维护引用稳定性的负担,它最终又把一部分运行时优化前移到 React Compiler。
这不是一部“React 功能大全”
本文选择那些改变了后续决策空间的节点。某个版本即使新增了大量 API,只要没有改变 React 对“组件、工作、宿主、时间或网络边界”的理解,就不会得到同等篇幅。相反,一个早期 PR 即使当时不能运行真实应用,只要它第一次把未来架构的关键约束写进代码,就可能占据整节。
每章使用三类证据标签:
史料事实:可以从提交、PR、RFC、发布说明或同期官方文章直接确认。
参与者回忆:来自项目创建者或维护者的事后叙述。它有一手价值,但也可能受到记忆和后见之明影响。
本文判断:基于多份材料做出的工程解释。它不是项目方原话,读者可以不同意。
完整资料索引见资料与注释,研究过程见仓库中的 research/react/。
章节地图
| 章节 | 时间焦点 | 要回答的问题 |
|---|---|---|
| 第一章 | 2010–2012 | Bolt、XHP、FaxJS 和函数式语言如何汇合成 React 的前身? |
| 第二章 | 2013–2014 | 为什么 React 故意把标记、逻辑和组件放回 JavaScript? |
| 第三章 | 2013–2015 | element、key、事务与合成事件怎样支撑“重新 render”? |
| 第四章 | 2014–2016 | React 拒绝成为完整框架,如何同时催生创新和碎片化? |
| 第五章 | 2013–2016 | 从 iOS 原型到 React Native,DOM 如何变成一个可替换宿主? |
| 第六章 | 2015–2017 | 为什么旧 reconciler 不能靠局部优化获得可中断渲染? |
| 第七章 | 2017–2020 | 如何在重写内核后仍让绝大多数应用平滑升级? |
| 第八章 | 2018–2019 | Hook 为什么依赖调用顺序,它解决和制造了什么问题? |
| 第九章 | 2018–2022 | “Concurrent Mode”为何被拆成渐进启用的一组 feature? |
| 第十章 | 2020–2026 | SSR、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. 从人工优化,转向编译分析
PureComponent、memo、useMemo 和 useCallback 都要求开发者理解引用身份。React Compiler 试图通过控制流、数据流和可变性分析自动划分 reactive scope。它没有替代 reconciler,而是把“哪些值可以复用”的部分判断从人移交给构建时工具。
Code Time Machine:建议按这个顺序读源码
| 快照 | 入口 | 观察重点 |
|---|---|---|
| 2013 初始公开版 | ReactCompositeComponent.js | render 已被要求无副作用;class、mixin 与生命周期协议同时存在。 |
| 2013 初始公开版 | ReactReconcileTransaction.js | DOM 更新、事件开关和 mount-ready 回调如何被事务包装。 |
| 2013 初始公开版 | EventPluginHub.js | 事件提取、排队和派发为什么被设计成插件管线。 |
| 2016 Fiber 起步 | PR #6690 | Noop renderer、nextUnitOfWork 与按 deadline 让出执行权。 |
| 2016 双缓冲树 | PR #6981 | alternate、current/work-in-progress 两棵树和对象复用。 |
| 2016 提交阶段 | PR #7154 | 先完成整棵树的协调,再沿副作用链调用宿主环境。 |
| 2019 Hooks | ReactFiberHooks.js@v16.8.0 | Hook 链表、dispatcher、调用顺序校验和 render-phase update。 |
| 2022 Lanes | ReactFiberLane.new.js@v18.2.0 | 多种工作如何编码到位掩码,并选择下一批 lanes。 |
| 2024–2026 Flight | ReactFlightServer.js@v19.2.0 | Server/Client reference、chunk、thenable 与流式协议的交汇。 |
| 2025 Compiler | HIR.ts@v19.2.0 | AST 如何降低为 CFG/HIR,再变成 reactive scopes 并回写 AST。 |
阅读这些快照时,不要先问“函数最终怎么跑完”,而要问四个问题:状态存在哪里?工作如何表示?什么时候允许产生副作用?哪个边界由宿主或框架负责?React 的每次重大演化,几乎都在重新回答其中至少一个。
图解入口
阅读路线
第一次系统理解 React,可按章节顺序阅读。已经熟悉日常 API、想研究实现,可先读第三、六、八、九、十章,再回到一、二章理解这些约束为何出现。正在做 bundler、renderer 或全栈框架,建议重点关注第五、九、十、十一章:React 对外看似是组件 API,对集成者暴露的却是一组不断扩张的调度、模块引用、流协议和编译契约。
下一章从 React 还不叫 React 的时候开始:不是从某个天才瞬间开始,而是从一套已经难以维护的广告界面系统开始。