1. derivedStateOf 工作原理:输入依赖、计算推导、快照系统、跳过重组条件

1.1 核心概念

derivedStateOf 是 Compose 中用于将多个可观察状态转换为一个新状态的管道。它不会主动触发重组,仅在产出值被读取且依赖发生变化时才重新计算,从而减少不必要的组合开销。

关键角色:
  • 输入依赖(Input Snapshots)—— 被监听的可变状态。
  • 计算推导(Derivation Lambda)—— 纯函数,将输入映射为输出。
  • 快照系统(Snapshot System)—— Compose 的响应式追踪引擎。
  • 跳过重组条件(Recomposition Skip)—— 当推导结果不变时,跳过后续组合阶段。

1.2 输入依赖与快照追踪

每次访问 derivedStateOf 的 .value 时,Compose 的快照系统会记录其推导 lambda 内部读取了哪些SnapshotState 对象。这些被读取的状态即为“输入依赖”。当任何一个依赖发生变更时,快照系统标记派生状态为“脏”,在下一次读取时重新执行推导。

// 示例:输入依赖追踪
var count by remember { mutableStateOf(0) }
var step by remember { mutableStateOf(1) }

val derivedValue = remember { derivedStateOf { count * step } }
// 读取 derivedValue.value 时,快照记录依赖:count, step

1.3 计算推导与纯函数约束

推导 lambda 必须是纯函数:相同的输入始终产生相同的输出,且不产生副作用(如修改状态、发起网络请求)。这保证 Compose 可以安全地缓存计算结果。

属性要求违反后果
幂等性同一依赖值返回相同结果状态不一致、无限重组
无副作用不修改任何外部状态快照追踪失效、界面跳跃
计算轻量避免复杂循环/IO帧率下降、ANR 风险

1.4 跳过重组条件

当派生值的上一个缓存结果与最新推导结果通过 == 比较相等时,Compose 不会将该派生值标记为“需要重组”。这一机制大幅减少了重组层级——即使输入依赖发生频繁变化,只要最终推导结果不变,下游 UI 就不会重建。

优化要点: 将复杂计算结果包裹在 derivedStateOf 中,可以避免每次重组都执行相同计算;配合 remember 进一步缓存 lambda 实例。

1.5 完整数据流

下图展示一次完整的推导-读取-跳过循环:

derivedStateOf 数据流与跳过重组 输入依赖 A 输入依赖 B 推导 lambda 快照系统 输出值 结果比较 跳过下游重组 &; 只有当快照检测到任一依赖变更,且推导结果不等于上一次缓存时, 下游 Composable 才会重组。否则直接跳过整个组合阶段。

1.6 常见错误与稳定性优化

陷阱:在推导 lambda 中执行副作用
// ❌ 错误:修改外部状态
val bad = derivedStateOf {
    if (x == 0) y++   // 副作用,破坏纯函数
    x + y
}

// ✅ 正确:仅计算
val good = derivedStateOf { x + y }
稳定性提示:
  • 始终使用 remember 包裹 derivedStateOf,避免在重组中重复创建。
  • 对于集合或对象比较,确保正确实现 equals(),否则跳过重组可能失效。
  • 如果推导计算代价高(如排序、过滤),考虑加入防抖或节流逻辑,避免高频依赖变更导致频繁重算。

1.7 与普通 State 的对比

特性普通 mutableStateOfderivedStateOf
触发方式写操作立即触发读取 + 依赖变更触发
计算时机每次写入仅当被读取且依赖变化
跳过重组不支持支持(结果相等时跳过)
适用场景直接持有的数据基于其他状态的派生数据

理解以上四个要素后,您可以精确地控制派生状态的生命周期和计算开销,避免因滥用 derivedStateOf 带来的性能反模式。