Stack Reconciler(栈协调器)核心原理
- 触发 setState/forceUpdate 后,从根组件开始深度优先递归遍历所有子组件 VNode;
- 每进入一个子组件,函数入栈;组件处理完毕,函数出栈;
- 整棵树的 Diff、实例更新是一次性同步执行,全程阻塞浏览器主线程。
Fiber 架构解决的核心问题
Fiber 主要解决旧 Stack 架构同步递归渲染阻塞主线程的痛点(基于函数递归调用栈实现同步渲染,渲染一旦启动无法中断,大数据场景阻塞主线程),把递归拆成可中断的分片任务,实现渲染暂停 / 恢复 / 优先级调度,是 React18 并发渲染、Suspense数据挂起、useTransition 的底层基石。
- 解决旧 Stack 架构「一次性递归、阻塞主线程」痛点📌
React15 是栈递归渲染,组件树递归一旦开始不能中断:渲染超大列表 / 深层组件时,JS 主线程被长时间占用,浏览器无法处理点击、输入、滚动,页面卡顿掉帧。
Fiber 把整棵组件树拆分一个个 Fiber 节点任务,用时间切片拆分任务,每执行一小段就交还主线程,保证浏览器及时响应用户交互。 - 实现渲染任务的优先级调度💡
Fiber 架构支持任务分级:用户输入 / 点击等高优任务可插队打断低优渲染任务,优先执行交互;低优任务暂停后可后续恢复执行,从底层支撑useTransition/useDeferredValueAPI。 - 拆分「协调 (Reconcile)」和「提交 (Commit)」两个阶段
协调阶段(可中断):遍历 Fiber 树、计算 DOM 补丁,可随时暂停、插队、重启;
提交阶段(不可中断):一次性批量操作真实 DOM。
既保证可中断优化体验,又避免 DOM 频繁操作损耗性能,同时支撑 Suspense 挂起逻辑。 - 为 React18 新特性提供底层能力
Fiber 是并发渲染、Suspense 组件懒加载、数据挂起、自动批处理更新的底层实现基础,没有 Fiber 就无法实现以上新特性。
Fiber 节点 是 JS 对象,保存组件类型、props、子节点、副作用标记,替代原先递归栈存储信息。
渲染 / 调和阶段 (Render/Reconciliation Phase):这是一个可中断的阶段。在此阶段,React 会构建出 "工作正在进行中" 的 fiber 树(work-in-progress tree),计算出所有节点的变更。由于这个过程可能会被暂停、重做甚至丢弃,所以它不会产生任何用户可见的副作用(比如操作 DOM)。
提交阶段(Commit Phase):这是一个不可中断的同步阶段。一旦调和阶段完成,React 就会进入提交阶段,将计算出的所有变更一次性应用到 DOM 上。这个过程必须是同步的,以确保 UI 的一致性,避免用户看到渲染了一半的、不完整的界面。
可中断的 Fiber 架构
React 用 Fiber 节点树来表示组件结构,把一次渲染拆成了一个个小的工作单元:
- 每个单元的执行时间极短(几毫秒)
- 执行完一个单元后,React 会检查主线程是否有更高优先级任务
- 如果有,就暂停当前渲染,先处理高优先级任务,之后再继续
双缓冲更新
并发渲染下,React 会维护两棵 Fiber 树:
当前显示的树(Current)
正在后台渲染的树(Work-in-Progress)
低优先级更新会在后台树上执行,渲染完成后再一次性切换到前台,不会阻塞主线程。
Fiber 节点和链表结构的任务拆分
Fiber 架构把树形结构转换成了链表结构,每个 Fiber 节点包含三个关键指针:
return指向父节点,child指向第一个子节点,sibling指向下一个兄弟节点。
遍历时从根节点开始,优先向下找child,没有child就找sibling,没有sibling就通过return回到父节点再找父节点的sibling。
这种遍历方式每次只处理一个节点,处理完一个节点就可以检查时间是否用完,用完了就中断并保存当前节点位置,下次恢复时从中断点继续。
每个 Fiber 节点还存储了pendingProps、memoizedProps、memoizedState等字段,用于对比更新前后的变化。
【节点1】
return: null
child: 节点2
sibling: null
│
▼ child
【节点2】────────────────────sibling────────────→【右节点2】
return: 节点1 return: 节点1
child: 节点3 child: 节点5
sibling: 右节点2 sibling: null
│ │
▼ child ▼ child
【节点3】──sibling→【节点4】 【节点5】──sibling→【节点6】
return: 节点2 return: 节点2 return: 右节点2 return: 右节点2
child: null child: null child: null child: null
sibling: 节点4 sibling: null sibling: 节点6 sibling: null
执行流程
从根 Fiber 节点开始,处理单个节点;
处理完成后检测浏览器剩余时间片,
时间充足,寻找下一个待处理节点,
寻找优先级 :child子节点 > sibling兄弟节点 > return回溯父节点;
节点 1 → 节点 2 → 节点 3 → 节点 4
回溯节点 2 → 取节点 2 兄弟右节点 2 → 节点 5 → 节点 6
时间耗尽,保存当前节点作为断点,让出主线程,暂停渲染;
浏览器主线程空闲后,从保存的断点节点继续遍历,无需从头执行。
┌─────────────────────────────────────────────────────────┐
│ Fiber节点容器 │
├───────────────┬─────────────────────────────────────────┤
│ 链表指针区 │ return / child / sibling │
├───────────────┼─────────────────────────────────────────┤
│ 数据对比区 │ pendingProps、memoizedProps、memoizedState │
├───────────────┼─────────────────────────────────────────┤
│ 双缓存指针 │ alternate(双向指向另一棵缓存树对应节点) │
├───────────────┼─────────────────────────────────────────┤
│ 副作用标记 │ flags(标记DOM增/删/改、生命周期执行) │
└───────────────┴─────────────────────────────────────────┘
优先级调度和任务插队
React 设计了三个层次的事件优先级。
最高优先级是离散事件 ,比如点击、输入框变化,需要立即响应。
中间优先级是连续事件 ,比如滚动、拖拽。
最低优先级是空闲任务 ,比如网络请求回来的数据更新。
Scheduler 调度器维护了一个任务队列,每次执行任务前检查当前时间是否还有剩余时间片,如果没有就交出主线程。
高优先级任务到达时,会打断正在执行的低优先级任务,低优先级任务被取消,然后重新开始执行高优先级任务。
这意味着低优先级任务已经构建的 workInProgress 树会被丢弃,等所有高优先级任务处理完后再重新构建。
为了避免用户感知到内容跳变,React 需要确保 commit 阶段的内容是最终确定的。
双缓存树的渲染流程
current树代表当前屏幕上已经渲染的内容,workInProgress树是在内存中构建的新树。
首次渲染时,createRoot创建根节点,初始化current树。
调度更新时,复制current树的根节点创建workInProgress树,然后遍历workInProgress树处理更新。
所有更新计算完成后进入commit阶段。commit阶段分三步。
第一步before mutation,调用类组件的getSnapshotBeforeUpdate。
第二步mutation,执行DOM的增删改。
第三步layout,调用useLayoutEffect和componentDidMount。
这三步不能中断,因为每一步都依赖前一步的执行结果。
commit完成后,workInProgress树成为新的current树。
