关键设计决策和原因
1. 为什么选择 Fiber 架构?
1.1 问题背景
React 15 及之前的版本使用递归渲染,存在以下问题:
- 不可中断:渲染过程一旦开始就不能中断
- 阻塞主线程:长时间渲染会阻塞主线程
- 用户体验差:用户交互会被延迟
1.2 解决方案
Fiber 架构通过以下方式解决这些问题:
- 可中断渲染:将渲染工作分成多个小任务,可以暂停和恢复
- 优先级调度:根据优先级决定先执行哪些任务
- 时间切片:将长时间任务分成多个小任务
1.3 设计原因
根据 Fiber Principles:
- 增量渲染:支持将渲染工作分成多个小任务
- 优先级调度:支持根据优先级调度任务
- 错误边界:支持错误边界捕获更新错误
- 向后兼容:保持与现有 API 的兼容性
2. 为什么使用双缓冲?
2.1 问题背景
每次更新都创建新的 Fiber 树会导致:
- 内存分配:频繁的内存分配和回收
- 性能问题:GC 压力大
- 无法复用:无法复用之前的节点
2.2 解决方案
使用双缓冲技术:
- Current Tree:当前显示在屏幕上的树
- WorkInProgress Tree:正在构建的新树
- alternate 字段:两个树之间的连接
2.3 设计原因
- 内存优化:复用节点,减少内存分配
- 性能优化:减少 GC 压力
- 一致性:保证更新的一致性
3. 为什么使用 Lanes 而不是 ExpirationTime?
3.1 ExpirationTime 的问题
React 16 使用 ExpirationTime 管理优先级,存在以下问题:
- 精度问题:ExpirationTime 是时间戳,精度有限
- 优先级冲突:多个更新可能有相同的 ExpirationTime
- 难以扩展:难以支持更多的优先级级别
3.2 Lanes 的优势
React 18 使用 Lanes(车道)系统:
- 位掩码:使用位掩码表示优先级,支持多个优先级
- 高效操作:位运算比时间戳比较快
- 易于扩展:可以轻松添加新的优先级级别
3.3 设计原因
根据 Lanes RFC:
- 更好的优先级管理:支持多个优先级级别
- 更高效的比较:位运算比时间戳比较快
- 更灵活的调度:可以更灵活地调度任务
4. 为什么 Hooks 必须按顺序调用?
4.1 问题背景
Hooks 通过每次渲染的调用顺序对应 Fiber 上链表槽位;条件/循环中多调或少调会导致状态串线(React 19 的 use 是例外)。详见 04-Hooks §4。
4.2 解决方案
强制 Hooks 按顺序调用:
- Rules of Hooks:Hooks 必须在顶层调用
- 开发时检查:开发模式下检查 Hooks 调用顺序
- 错误提示:提供清晰的错误信息
4.3 设计原因
根据 Hooks RFC:
- 简单实现:通过调用顺序识别 Hooks,实现简单
- 性能优化:不需要额外的标识符
- 易于理解:规则简单明了
5. 为什么使用链表存储 Hooks?
5.1 问题背景
Hooks 数量在运行时确定,不能使用固定大小的数组。
5.2 解决方案
使用链表存储 Hooks:
javascript
type Hook = {
memoizedState: any,
next: Hook | null,
};5.3 设计原因
- 动态数量:支持动态数量的 Hooks
- 顺序稳定:保持 Hooks 的调用顺序
- 高效查找:通过遍历链表访问 Hooks
6. 为什么 Diff 算法只比较同层节点?
6.1 问题背景
跨层级比较的复杂度是 O(n³),性能很差。
6.2 解决方案
只比较同一层级的节点:
- 同层比较:只比较同一层级的节点
- 类型检查:类型不同直接替换
- key 优化:使用 key 快速识别节点
6.3 设计原因
- 性能优化:同层比较的复杂度是 O(n)
- 实际场景:大多数情况下组件不会跨层级移动
- 简单实现:实现简单,易于维护
7. 为什么使用 Virtual DOM?
7.1 问题背景
直接操作 DOM 很昂贵:
- 重排和重绘:每次 DOM 操作都可能触发重排和重绘
- 跨平台:不同平台的 DOM API 不同
- 批处理困难:难以批量处理多个更新
7.2 解决方案
使用 Virtual DOM:
- 抽象层:Virtual DOM 是 DOM 的抽象
- 批量更新:可以将多个更新合并
- 跨平台:同一套 Virtual DOM 可以渲染到不同平台
7.3 设计原因
- 性能优化:减少 DOM 操作次数
- 跨平台:支持 Web、Native、Canvas 等平台
- 开发体验:提供声明式的开发方式
8. 为什么使用 Scheduler?
8.1 问题背景
浏览器是单线程的,长时间任务会阻塞主线程。
8.2 解决方案
使用 Scheduler 调度任务:
- 时间切片:将长时间任务分成多个小任务
- 优先级调度:根据优先级调度任务
- 让出控制权:在适当时机让出控制权
8.3 设计原因
- 用户体验:避免阻塞主线程
- 响应性:优先处理用户交互
- 性能优化:充分利用浏览器的时间切片
9. 为什么引入 Server Components?
9.1 问题背景
客户端 JavaScript 包越来越大,影响性能。
9.2 解决方案
引入 Server Components:
- 减少包大小:Server Components 不在客户端
- 直接访问数据:可以直接访问数据库和文件系统
- 更好的性能:减少网络请求和客户端计算
9.3 设计原因
- 性能优化:减少客户端 JavaScript 包大小
- 开发体验:简化数据获取逻辑
- 未来方向:是 React 的未来方向
10. 为什么使用 Suspense?
10.1 问题背景
异步数据获取和代码分割需要单独处理。
10.2 解决方案
使用 Suspense 统一处理:
- 统一 API:统一的异步处理 API
- 更好的 UX:提供更好的加载体验
- 简化代码:简化异步数据获取代码
10.3 设计原因
- 统一处理:统一处理异步数据和代码分割
- 更好的 UX:提供更好的加载体验
- 简化代码:简化异步数据获取代码
11. 总结
React 的关键设计决策:
- Fiber 架构:支持可中断渲染和优先级调度
- 双缓冲:优化内存使用和性能
- Lanes 系统:更好的优先级管理
- Hooks:简化状态管理和逻辑复用
- Virtual DOM:跨平台和性能优化
- Scheduler:时间切片和优先级调度
- Server Components:减少包大小和提升性能
- Suspense:统一异步处理
这些设计决策都是为了提供更好的性能、开发体验和用户体验。