第七章:React 16,一次藏在兼容 API 后面的换心手术
2017 年 9 月 26 日发布的 React 16,表面上像一次功能丰富的 major release:fragment、string return、error boundary、portal、自定义 DOM attribute、更快且可流式输出的 server renderer、更小的 bundle,以及 MIT license。
发布说明直到后半才点出最危险的事实:React 16 是第一个建立在 Fiber 上的版本,“我们重写了 React”。更不寻常的是,团队希望大多数没有 warning 的 React 15.6 应用直接升级。
这是一种刻意的工程策略:先把公共语义当作约束,再用全新内核重新实现它。重写不是让用户为架构付费,而是由维护者承担兼容成本。
新功能证明了新架构,而不是解释新架构
Fragment 允许组件返回多个 sibling,不再为了满足“单根节点”插入额外 DOM。Portal 允许 children 被提交到组件 DOM 层级之外的 container,同时保留 React 树中的 context 和事件关系。Error Boundary 则能捕获子树 render/lifecycle 错误并显示 fallback。
这些能力各自有产品价值,也验证 Fiber 的通用性:
- Fiber child/sibling 结构比旧实例模型更自然地表达多返回值。
- HostRoot/HostPortal 可把同一 React 树的工作提交到不同 container。
- 错误可以沿
return链向上捕获,标记边界更新,而不是让内部状态半损坏。 - render 与 commit 分离后,失败的候选树可以被放弃,已提交树仍是明确基线。
Error Boundary 尤其体现“错误也是一次更新”。边界并非 JavaScript try/catch 包住异步事件,而是 reconciler 在遍历中发现失败,找到能处理它的祖先,安排 fallback state,再重新协调。
Server Renderer 也被重写
React 16 同时替换了 server renderer,支持 streaming,并移除旧 checksum hydration 机制。服务器不再必须等完整字符串生成后才发送第一字节,客户端 hydration 也尝试复用已有 DOM。
这套 renderer 还不是 React 18 的 Fizz:它无法以 Suspense boundary 为单位独立等待和补充内容。但它确立了一个方向——服务端输出不是附属 helper,而是需要针对流、背压和 hydration 设计的独立执行器。
后来 Fizz 与 Flight 都没有直接复用 Fiber DOM commit。它们共享 component/element 语义,却针对服务端约束建立自己的 request、task、segment 与 chunk 模型。多 renderer 的边界因此继续深化。
MIT License:非技术决策也会改变采用路径
React 早期使用 BSD 加附加专利条款,引发企业与开源社区长期担忧。2017 年 Facebook 宣布 React 16 及 React 15.6.2 改用 MIT。发布说明把它列为 major change 之一。
许可证不会改变 reconciler 的算法,却会改变哪些组织愿意把 React 作为长期基础设施。对成熟开源项目而言,治理、商标、专利与发布政策也是架构的外部边界。2026 年 React 进入独立 Foundation,是同一问题更晚的回答。
“Async Rendering”尚未开启
React 16 文章把 cooperative async rendering 描述为最令人期待的未来,但明确说 16.0 不启用异步功能。Fiber 首先以同步方式替换旧 reconciler,确保行为兼容;只有内核稳定后,团队才逐步打开调度能力。
这个顺序避免同时改变实现和用户可见时序。若 Fiber 与并发语义一起发布,任何 bug 都难以判断来自重写错误还是新的重放规则。
本文判断:React 16 的关键产品不是“并发”,而是“可并发的内部表示已经进入稳定渠道”。架构选择权先上线,用户 feature 后上线。
旧 Lifecycle 与可重放 Render 冲突
class lifecycle 中,componentWillMount、componentWillReceiveProps 和 componentWillUpdate 都发生在 commit 之前。同步栈时代,一次更新通常按顺序走完,开发者很容易在这些方法里发请求、订阅或改全局变量。
可中断 render 下,pre-commit 工作可能:
- 被调用后暂停。
- 因更高优先级更新而重启。
- 完成计算却最终不 commit。
- 在开发模式下被额外调用以暴露问题。
若这些 lifecycle 含不可重复副作用,就可能发出重复请求或留下未清理订阅。2018 年官方《Update on Async Rendering》宣布给它们增加 UNSAFE_ 前缀,并提供更安全迁移方式。
团队没有立即删除 API。文章提到 Facebook 内部有超过五万个 React component,需要 warning、codemod 与多个版本的迁移窗口。这再次体现 React 的升级方法:把破坏性变化拉长,让大型代码库能在日常开发中逐步修正。
Render 必须纯,不再只是建议
初始公开版已经写着 render 不应有副作用;Fiber 让这条建议成为执行模型要求。React 可以在 commit 前多次调用 render,只有最后一次完成结果会被用户看到。
这并不意味着 render 只能做常量运算。它可以读取 props、state、context,创建 element,执行确定性派生计算,甚至在 Suspense 模型中读取会导致等待的资源。不可接受的是把外部世界永久改变:修改 DOM、订阅、记录不可幂等事件、发起无法去重的写操作。
class 时代,componentDidMount/componentDidUpdate 是安全的提交后出口;Hooks 时代,layout/passive effect 接替该职责。React 的 API 变化围绕同一分界持续收敛。
React 17:一个没有新 feature 的过渡版本
2020 年 React 17 被称为“没有新功能”的 release,主要目标之一是让多个 React 版本更容易渐进升级。事件委托从全局 document 移到 root container,减少嵌套版本相互干扰。
这与 React 16 的经验一致:成熟基础设施的重大价值有时不是增加 API,而是降低迁移耦合。React 18 的 concurrent root、未来 framework adoption,都需要应用能够分 root、分页面或分团队逐步进入新语义。
兼容背后的真实成本
React 16 能保持公共 API,大量复杂度被移进内部:
- current/WIP 两棵树之间复制和恢复 state。
- 对 update queue 按优先级跳过、保留与重放。
- 在不同 phase 精确调用 lifecycle、ref 和 host mutation。
- 对 error、unmount、portal、context 和 hydration 保持旧行为。
- 为多个 renderer 构建不同 host config fork。
用户看到的是“升级后大多能跑”,维护者面对的是一个比旧 reconciler 更难局部理解的状态机。
本文判断:React 长期稳定的 API 并不代表项目内部保守。恰恰相反,稳定外壳给了团队重写内核的空间;代价是实现必须同时背负历史语义和未来能力。
Fiber 已经解决“工作如何暂停”,但 class 仍把可复用状态逻辑困在实例和 lifecycle 中。下一章的 Hooks 不会替换 Fiber;它会利用 Fiber 保存状态,并把组件作者的主要抽象从实例方法改成按顺序执行的函数调用。