React 更新流程

66 阅读20分钟

这玩意真不是人看的,太费神了

Mermaid (1).jpg

这样看更清楚:

image.png

主流程(从外部触发到调度)

1. 外部触发(Entr)

  • 目的:代表用户交互、网络响应、定时器、外部 API 调用等触发应用状态改变的事件源。
  • 为什么要这么做:必须有“入口”触发更新,才能启动 React 的更新流程。
  • 如果没有:应用无法响应用户或外部变化,UI 不会更新。
  • 解决的问题:把“事件源”抽象出来,统一进入后续的更新系统。
  • 还存在哪些问题:来源多、频率高时会产生大量更新,需要调度和合并策略以避免过度渲染。

2. Enqueue Update(将更新加入队列)

  • 目的:把本次状态/属性更新封装成 Update 并加入对应 Fiber 的 UpdateQueue。
  • 为什么要这么做:保证更新不是直接、立刻修改 DOM,而是进入可管理的队列以便调度与合并。
  • 如果没有:每次更新都会同步触发渲染,性能会极差,且难以合并多次快速更新。
  • 解决的问题:实现批量合并、优先级排序、可中断执行等能力的基础。
  • 还存在哪些问题:队列管理复杂(如并发模式下的跨渲染更新),需要仔细处理顺序和优先级。

3. scheduleUpdateOnFiber(调度更新)

  • 目的:将 enqueue 的更新转为一个调度任务(标记受影响的 root、设置 lanes/expiration 等),把工作安排给调度器。
  • 为什么要这么做:把“有更新”变成“需要执行的工作”,并把优先级信息传递给调度器。
  • 如果没有:无法决定何时或以何种优先级去执行更新;更新可能立刻阻塞线程或相互冲突。
  • 解决的问题:把更新纳入统一的调度系统,支持并发、优先级和中断。
  • 还存在哪些问题:调度决策复杂(优先级、过期时间、交互感知),调度器的实现需要权衡响应性与吞吐量。

4. 确定优先级 / lanes / expirationTime(Priority)

  • 目的:计算并分配更新的优先级(lane)和到期时间,用于后续的调度选择和抢占。
  • 为什么要这么做:不同更新的重要性不同(例如用户输入 vs 后台数据刷新),需要不同处理策略。
  • 如果没有:所有更新按同一策略处理,会导致关键交互被延迟或背景任务阻塞主流程。
  • 解决的问题:允许高优先级更新优先执行,低优先级可延后或合并,从而保证交互流畅。
  • 还存在哪些问题:优先级分配并非完美(可能饥饿低优先级任务),且计算策略复杂,涉及 expirationTime 的调优。

同步与并发渲染选择

5a. performSyncWorkOnRoot(同步渲染)

  • 目的:以同步阻塞的方式完成整个 root 的渲染和提交(适用于高优先级或强制同步场景)。
  • 为什么要这么做:当更新必须立刻反映(例如 flushSync、输入事件的高优先级处理)时,使用同步以保证一致性。
  • 如果没有:在强制同步场景下若使用并发,会导致 UI 延迟或出现不一致的中间状态。
  • 解决的问题:提供确定性的、立即可见的更新路径。
  • 还存在哪些问题:会阻塞主线程、影响帧率、降低并发能力;滥用会造成卡顿。

5b. performConcurrentWorkOnRoot(并发渲染)

  • 目的:以可中断、分片执行的方式进行渲染,允许更高优先级的任务中断当前渲染。
  • 为什么要这么做:提高主线程可响应性,避免长任务阻塞 UI;支持渐进式呈现(progressive rendering)。
  • 如果没有:用户交互可能被长时间渲染阻塞,导致卡顿或掉帧。
  • 解决的问题:平衡渲染吞吐量与响应性,允许优先处理关键更新。
  • 还存在哪些问题:实现更复杂(需要保存中断点、恢复状态),并发渲染可能导致中间状态可见性和调试难度上升。

20. 并发中断(Interrupt)

  • 目的:在并发渲染过程中,当出现更高优先级更新时暂停当前工作并重新调度。
  • 为什么要这么做:保证高优先级任务(如输入响应)能尽快被处理。
  • 如果没有:低优先级长任务会阻塞高优先级更新,交互体验变差。
  • 解决的问题:提高交互优先性,支持任务抢占。
  • 还存在哪些问题:频繁中断会导致进度浪费(重复工作),需要智能的中断与合并机制降低开销。

Render Phase(渲染阶段 — 构建 workInProgress 树)

6. renderRoot(准备新的 workInProgress root)

  • 目的:创建或重用一个 workInProgress root(root 的工作副本),开始渲染流程。
  • 为什么要这么做:在不改变当前 committed tree 的情况下构建新的 UI 状态,支持中断与回滚。
  • 如果没有:任何渲染都会直接操作当前树,无法安全中断或回滚。
  • 解决的问题:提供可中断的渲染工作空间,避免在渲染中破坏已提交的树。
  • 还存在哪些问题:需要额外内存维护两个树(current/alternate),以及管理切换的复杂性。

7. beginWork loop(开始工作循环)

  • 目的:对 workInProgress 树进行深度优先的遍历,处理每个 Fiber 的 beginWork 阶段,生成子树。
  • 为什么要这么做:计算每个组件根据新 props/state 应该如何表现(是否需要更新、生成新的子 Fiber 等)。
  • 如果没有:无法逐个计算组件的变化;渲染过程就无法细化到每个组件。
  • 解决的问题:把渲染拆成单位化工作(unit of work),便于中断、优先级切换与调度。
  • 还存在哪些问题:循环需要管理复杂的状态(effect list、优先级),并处理 Hook、context 等多种副作用。

8. beginWork(协调子节点 / reconcile)

  • 目的:核心的协调(reconciliation)函数,决定当前 Fiber 是否需要更新、创建新的 child Fiber、标记 effects 等。
  • 为什么要这么做:对比旧树与新输入,最小化需要变更的节点(diff 算法的一部分)。
  • 如果没有:会导致盲目重建整个子树,导致性能损失或错误的 DOM 操作。
  • 解决的问题:实现高效的更新(最小化变更)、保留不变部分。
  • 还存在哪些问题:协调算法不是完美(比如 key 的误用会破坏复用),diff 是启发式而非完备证明正确。

9. 检查 Hook(HooksCheck)

  • 目的:判断当前函数组件是否使用 Hook,并根据 Hook 使用情况走不同的处理分支。
  • 为什么要这么做:Hook(useState/useEffect 等)需要在 render 中被解析并维护内部状态队列。
  • 如果没有:函数组件的状态、副作用、memo 等无法正确追踪与更新。
  • 解决的问题:为 Hook 提供解析、调度和状态更新的入口。
  • 还存在哪些问题:Hook 规则(调用顺序必须一致)带来使用约束;复杂 Hook 交互可能导致调试难度。

10. 解析 Hook / UpdateHooks(计算新状态,收集 effects)

  • 目的:按顺序执行 Hook‘s reducer/updates,计算 memoizedState,收集需要触发的副作用(effects)。
  • 为什么要这么做:Hook 的 state 更新需要被聚合并反映在 workInProgress 上,同时记录需要在 commit 时触发的 effect。
  • 如果没有:useState/useReducer 的更新不会被累积或正确应用,useEffect 的副作用时机也会错乱。
  • 解决的问题:保证 Hook 状态一致、延后执行副作用直到 commit 阶段。
  • 还存在哪些问题:复杂的异步更新与并发模式下的 Hook 更新管理比较棘手(例如新的 updates 在正在进行的 render 中到来),需要细节处理以避免丢失或乱序。

11. 无 Hook 处理(NoHooks)

  • 目的:针对没有 Hooks 的组件做更简单的处理(例如 class 组件或纯 JSX)——略去 Hook 解析步骤。
  • 为什么要这么做:减少不必要的开销,走轻量路径。
  • 如果没有:所有组件都必须走相同复杂路径,会浪费性能。
  • 解决的问题:保持性能优化的分支路径。
  • 还存在哪些问题:需要维护多套逻辑路径,增加实现复杂度。

12. completeUnitOfWork(完成当前单元工作)

  • 目的:完成当前 Fiber 的工作,处理副作用收集、bubble up 状态、构建 effect list,准备返回 parent。
  • 为什么要这么做:在深度优先遍历里,子节点处理完后需要把信息合并到父节点。
  • 如果没有:无法构建正确的 effect list 和父子间状态传播。
  • 解决的问题:把子树变更合并并传递回上层,最终产生一个树级别的 effect list。
  • 还存在哪些问题:effect list 的顺序和正确性很关键,错误会导致错乱的提交顺序或遗漏副作用。

13. 还有未完成 unit?(NextUnit)

  • 目的:决定是否继续循环处理下一个 unit(Fiber)。
  • 为什么要这么做:控制工作循环终止或继续,支持分片与中断。
  • 如果没有:循环要么无穷,要么一次性结束且不能中断。
  • 解决的问题:实现渲染的可中断性和分片执行。
  • 还存在哪些问题:如何选择合适的切片大小和中断点是性能调优的重要点。

14. render phase 完成(workInProgress tree 准备就绪)(RenderPhaseDone)

  • 目的:表示 render(协调)阶段完成,workInProgress tree 已准备好;下一步进入 commit。
  • 为什么要这么做:把渲染(计算 UI)与提交(实际 DOM 修改)明确分离,保证副作用在正确时机执行。
  • 如果没有:副作用可能在 render 阶段就发生,造成不可预测的副作用或读写冲突。
  • 解决的问题:提供阶段分离(pure render → side-effects commit),使渲染过程可预测且可恢复。
  • 还存在哪些问题:render 阶段仍可能抛出错误或被 Suspense 阻塞,需要后续处理。

Commit Phase(提交阶段 — 对 DOM 进行操作)

15. commitBeforeMutationEffects(commitBeforeMutationEffects — DOM 变更前快照)

  • 目的:执行 getSnapshotBeforeUpdate 这类在 DOM 被改变前需要读取的同步钩子,以获取变更前快照。
  • 为什么要这么做:部分 API 需要“变更前”状态(例如滚动位置)来计算更新后需要的调整。
  • 如果没有:在 DOM 修改后再读取快照会得不到正确的旧状态,失去所需信息。
  • 解决的问题:保证读取变更前的信息的时机正确,便于后续布局调整。
  • 还存在哪些问题:这些操作必须同步执行,可能阻塞主线程短时间;并发场景下快照策略更复杂。

16. commitMutationEffects(DOM 插入/更新/删除)

  • 目的:真正执行 DOM 变更(插入、替换、属性更新、删除),把 workInProgress 的变化映射到真实 DOM。
  • 为什么要这么做:render 只是计算差异,commit 才把差异应用到宿主环境(DOM、原生端、Canvas 等)。
  • 如果没有:用户看不到界面的变化。
  • 解决的问题:把纯计算与副作用分离,保证副作用一次性、可控地执行。
  • 还存在哪些问题:DOM 操作是昂贵的,需要尽量合并与最小化;且必须考虑布局抖动与重排(reflow)成本。

17. commitLayoutEffects(同步布局效果)

  • 目的:运行同步的 layout-effect(例如 useLayoutEffect)——这些副作用需要在 DOM 更新后立即运行,以同步测量或强制布局。
  • 为什么要这么做:某些副作用依赖立即读取更新后的 DOM(比如测量元素大小并立刻调整),需要同步执行以避免闪烁。
  • 如果没有:测量可能会读取到旧的 DOM 或导致视觉闪烁。
  • 解决的问题:为需要严格同步保证的副作用提供正确时机。
  • 还存在哪些问题:同步布局副作用会阻塞渲染,过度使用会造成性能问题,尽量使用 useEffect(异步)替代,除非真的需要同步。

18. flushPassiveEffects(异步副作用处理)

  • 目的:异步地执行被标记为被动(passive)的副作用,如 useEffect 的回调,通常在浏览器空闲或下一轮事件循环时执行。
  • 为什么要这么做:把不需要同步执行的副作用异步化,避免阻塞布局与交互,提升响应性。
  • 如果没有:被动副作用如果同步执行,会延长帧时间,导致卡顿。
  • 解决的问题:提高主线程可用性与 UI 流畅度,把次要副作用延后执行。
  • 还存在哪些问题:延迟可能带来可见性差(需要马上发生的副作用被延迟),以及复杂的副作用顺序问题(多个更新间的依赖)。

19. 完成更新 / 清理状态 / 调度下一个任务(Finalize)

  • 目的:清理工作树中临时字段(如 effect 列表中的标志)、更新 root 的 current 指针、并准备或调度后续任务。
  • 为什么要这么做:把一次渲染的临时数据收尾,保证下次渲染从正确状态开始。
  • 如果没有:会有内存泄漏、重复执行或状态不一致等问题。
  • 解决的问题:维护系统健康,清理脏数据,触发必要的后续任务(如处理剩余 low-priority 更新)。
  • 还存在哪些问题:清理和调度也有成本,尤其在大量小任务下需要高效实现。

30. 更新完成(End)

  • 目的:标记本次更新流程完全结束,应用已呈现到用户可见状态。
  • 为什么要这么做:为高层提供一致的生命周期边界,便于监控、日志和调试。
  • 如果没有:外部工具或逻辑无法正确判断更新何时完成,可能影响后续交互协调。
  • 解决的问题:为上层或开发者提供明确的完成点。
  • 还存在哪些问题:在并发环境下 “完成” 的定义要明确(例如部分被中断的 render),需要明确 semantics。

特殊 API 流程(右侧)

API 调用入口(A2_API)

  • 目的:表示 React 外部的 API 调用入口(startTransitionflushSyncunstable_batchedUpdates 等)。
  • 为什么要这么做:这些 API 改变默认调度行为(优先级、同步/异步、批量),需要在入口处统一处理。
  • 如果没有:API 的语义无法被调度器理解,行为不确定。
  • 解决的问题:把外部调用映射为调度策略变更或特殊标记。
  • 还存在哪些问题:API 组合(多个 API 嵌套)时语义复杂,需谨慎处理。

21. startTransition(标记过渡更新 — 低优先级)

  • 目的:把某些更新标记为“过渡/非关键”以降低优先级(用于保持输入响应优先)。
  • 为什么要这么做:允许把昂贵的 UI 更新(如导航、列表过滤)标记为可延后,从而优先处理关键交互。
  • 如果没有:用户输入可能被这些大更新阻塞,导致卡顿。
  • 解决的问题:提升感知性能,保证交互优先。
  • 还存在哪些问题:开发者需正确识别何为“过渡”,错误标记可能导致 UI 延迟显示或不一致。

22. flushSync(强制同步执行)

  • 目的:绕过并发调度,强制将包裹的更新同步执行并立即提交。
  • 为什么要这么做:有时需要确定性地立刻完成渲染(例如在测试或与非 React 代码交互时)。
  • 如果没有:某些需要即时反馈的场景无法保证。
  • 解决的问题:提供同步保证和兼容性手段。
  • 还存在哪些问题:滥用会破坏并发优势、降低性能、增加卡顿风险。

23. unstable_batchedUpdates(批量更新合并)

  • 目的:将多个更新合并为一次渲染/提交,避免重复渲染。
  • 为什么要这么做:在一次事件循环中批量更新能显著减小渲染次数,提高性能。
  • 如果没有:每次 setState 可能触发独立渲染,造成浪费。
  • 解决的问题:减少冗余渲染,提升吞吐量。
  • 还存在哪些问题:批量语义要与开发者期待一致(例如 setState 的返回值或副作用),并发模式下的批量行为有微妙差异。

并发与中断(右侧)

(已在 5b 与 20 里说明;补充说明)

  • 目的:通过中断和重新调度,保证更高优先级的更新及时响应。
  • 关键点:中断会保存部分工作进度(workInProgress),稍后继续或放弃。
  • 问题与挑战:保存/恢复的开销、重复工作、优先级 starvation(饥饿)防护。

Suspense 与错误边界(右侧)

24. 检查 Suspense(CheckSuspense)

  • 目的:检测渲染中是否有被 Suspense(资源未就绪)阻塞的子树。
  • 为什么要这么做:当异步资源(数据/代码分割)未就绪时,决定是否显示 fallback UI。
  • 如果没有:未就绪资源可能导致渲染异常或无限等待,不友好地阻塞整个 UI。
  • 解决的问题:优雅降级到 fallback,让应用继续保持响应性。
  • 还存在哪些问题:如何协调多个 Suspense 的优先级、延迟占位与占位抖动需要治理(例如避免闪烁)。

25. 显示 fallback UI(Fallback)

  • 目的:渲染占位 UI(spinner、占位骨架)替代被阻塞的子树。
  • 为什么要这么做:给用户及时反馈,避免空白或卡住的界面。
  • 如果没有:用户感知会认为应用挂起或崩溃。
  • 解决的问题:改善用户体验,掩盖异步加载延迟。
  • 还存在哪些问题:fallback 的视觉跳动可能影响体验;持续性的骨架显示也不是理想状态。

26. 继续提交(ContinueCommit)

  • 目的:当没有阻塞或 fallback 已处理后,继续正常的 commit 流程。
  • 为什么要这么做:确保在正确条件下执行 DOM 变更。
  • 如果没有:可能中断提交导致应用不一致。
  • 解决的问题:把 Suspense 决策与 commit 串联起来保证正确性。

27. 检查错误边界(ErrorBoundary)

  • 目的:判断 render/commit 是否抛出错误,是否存在可处理该错误的 Error Boundary。
  • 为什么要这么做:在出现运行时错误时,使用边界使应用局部降级而不是整页崩溃。
  • 如果没有:错误会冒泡到全局导致整个应用挂掉(白屏)。
  • 解决的问题:提高鲁棒性,允许 graceful recovery。
  • 还存在哪些问题:错误边界只能捕获生命周期与渲染中的错误,无法捕获事件回调内的异步错误;错误处理逻辑也要谨慎以避免掩盖真实问题。

28. 捕获并处理错误(HandleError)

  • 目的:把错误传递给 nearest ErrorBoundary,执行 fallback 渲染或清理。
  • 为什么要这么做:提供恢复路径并记录错误(日志、分析)。
  • 如果没有:用户体验受损,开发者难以追踪问题。
  • 还存在哪些问题:错误恢复有时会留下不一致状态,需要做好清理。

Fiber 节点内部结构(右侧底部)

对每个内部字段逐项解释其作用、为什么需要、如果没有会出现什么问题、仍然的限制。

UpdateQueue(UQ — 待处理更新)

  • 目的:保存针对该 Fiber 的待应用更新(setState、dispatch 等)。
  • 为什么要这么做:允许在不同渲染周期累积更新并在 render 时合并处理。
  • 如果没有:更新会立即应用,失去合并/优先级控制。
  • 解决的问题:收集更新并按顺序或优先级处理。
  • 还存在哪些问题:并发模式下 UpdateQueue 的合并与 replay 需要复杂逻辑,必须保证不丢失更新。

pendingProps(待处理的 props)

  • 目的:保存即将用于本次渲染的 props(尚未成为 committed 状态)。
  • 为什么要这么做:和 memoizedProps/currentProps 区分,便于比较与决定是否需要更新。
  • 如果没有:无法比较新旧 props,也无法正确决定变更最小集。
  • 解决的问题:实现高效 diff 的基础数据。
  • 还存在哪些问题:需要同步管理多个字段,增加复杂性。

memoizedState(记忆化状态)

  • 目的:保存上一次 render 后的 state(包括 Hook 的 memoizedState)。
  • 为什么要这么做:作为与 pending updates 比较和更新的基线,保证状态可持久化与可恢复。
  • 如果没有:无法保存组件状态或在中断后恢复。
  • 解决的问题:保持组件状态稳定。
  • 还存在哪些问题:在并发模式下状态的 replay/移植变得复杂(例如非线性更新序列)。

alternate(current 与 workInProgress 互为 alternate)

  • 目的:指向当前 Fiber 的另一个版本(current 与 workInProgress)以便双缓冲。
  • 为什么要这么做:支持在不破坏当前树的情况下构建新的 tree 并在 commit 时切换。
  • 如果没有:无法安全中断/回滚或在 render 期间保证一致性。
  • 解决的问题:实现可中断渲染的核心基础。
  • 还存在哪些问题:维护两份树需要额外内存与同步成本;某些优化要避免频繁复制。

lanes / expirationTime(优先级通道)

  • 目的:表示该 Fiber 相关更新的优先级集合与到期时间,用于调度决策。
  • 为什么要这么做:支持精细的优先级管理和过期机制。
  • 如果没有:无法对更新进行优先排序或防止更新无限延迟。
  • 解决的问题:能对不同来源/类型的更新进行分层处理。
  • 还存在哪些问题:lane 的合并/拆分策略较复杂,错误可能导致 starvation 或提前过期。

其他补充说明 / 总结要点

  • Render 与 Commit 的分离 是 React 能够做到并发渲染、可中断和回滚的基础:render 阶段纯计算(无副作用),commit 阶段执行副作用(DOM 改变、同步副作用、异步副作用)。

  • 优先级(lanes)系统 解决了“哪些更新先做”的问题,但引入了调度器复杂性:需要在响应性、吞吐量、及防止 starvation 三者之间权衡。

  • 并发带来的挑战:提高响应性但增加实现复杂度(中断、保存/恢复、重复工作、Hook 更新的一致性问题)。这也是为什么并发模式下有大量细节和边界条件需要处理(例如新的 updates 在渲染中到来、useEffect 的时间语义等)。

  • Suspense 与 ErrorBoundary 给出优雅的降级路径,但它们的语义、优先级和组合策略需要开发者在界面设计时考虑(避免过多的层叠 fallback 带来闪烁)。

  • Hook 的语义与限制:Hook 规则(固定调用顺序)是保证 Hook 内部状态映射正确的关键,但也约束了函数组件的编写方式。并发模式下,Hook 的更新与调度需要更细致的 replay 与合并策略。

  • 优化空间与未解决问题

    • 中断与重复工作的开销(需要更聪明的缓存/重用策略)。
    • 优先级算法的调优(避免 starvation,同步与异步优先级的折中)。
    • 在复杂异步数据加载场景下,如何让 Suspense 与数据缓存更好协作(减少闪烁和不必要的回退)。
    • 更好的开发者可观察性(例如更清晰地看到哪些更新被中断、为什么被降级)。