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 完整数据流
下图展示一次完整的推导-读取-跳过循环:
1.6 常见错误与稳定性优化
陷阱:在推导 lambda 中执行副作用
// ❌ 错误:修改外部状态
val bad = derivedStateOf {
if (x == 0) y++ // 副作用,破坏纯函数
x + y
}
// ✅ 正确:仅计算
val good = derivedStateOf { x + y }
稳定性提示:
- 始终使用
remember包裹derivedStateOf,避免在重组中重复创建。 - 对于集合或对象比较,确保正确实现
equals(),否则跳过重组可能失效。 - 如果推导计算代价高(如排序、过滤),考虑加入防抖或节流逻辑,避免高频依赖变更导致频繁重算。
1.7 与普通 State 的对比
| 特性 | 普通 mutableStateOf | derivedStateOf |
|---|---|---|
| 触发方式 | 写操作立即触发 | 读取 + 依赖变更触发 |
| 计算时机 | 每次写入 | 仅当被读取且依赖变化 |
| 跳过重组 | 不支持 | 支持(结果相等时跳过) |
| 适用场景 | 直接持有的数据 | 基于其他状态的派生数据 |
理解以上四个要素后,您可以精确地控制派生状态的生命周期和计算开销,避免因滥用 derivedStateOf 带来的性能反模式。