【LiveStates 03】拒绝无效重绘:利用 LiveCompute 实现手术刀级 UI 刷新

10 阅读4分钟

【LiveStates 03】拒绝无效重绘:利用 LiveCompute 实现手术刀级 UI 刷新

【LiveStates 01】别再手动 watch 了:开启 Flutter “自动追踪” DX 革命

【LiveStates 02】Zones 不止于异常捕获:揭秘 LiveStates 自动追踪黑科技

在前两篇文章中,我们聊了 live_states 的“触碰即追踪”和它背后的 Zone 机制。今天我们要深入探讨一个更进阶、也更关乎应用性能的话题:派生状态(Derived State)的性能陷阱。

在复杂的业务场景下,我们的 UI 往往不直接展示原始数据,而是展示“加工后”的结果。比如:

  • 购物车里的商品总价(依赖商品列表)。
  • 当前搜索关键词过滤后的列表(依赖列表和关键词)。
  • 提交按钮是否可点击(依赖多个输入框的校验结果)。

如果你处理不当,这些派生状态就会变成性能的“蝴蝶效应”中心,让你的 App 在不知不觉中变卡。

1. 传统方式的“蝴蝶效应”

在大多数 MVVM 方案中,我们习惯用 getter 来处理派生状态:

// 这种写法你一定见过
class CounterVM extends LiveViewModel {
  late final counter = LiveData<int>(0, owner);

  // 派生状态:判断是否为偶数
  bool get isEven => counter.value % 2 == 0;
}

看起来很清爽?但请注意,当你在 UI 中使用 isEven 时,由于它内部读取了 counter.value,这意味着 UI 实际上是在监听 counter

这就产生了一个严重的问题:

  1. counter 从 2 变到了 4。
  2. isEven 的结果仍然是 true(没变)。
  3. 但是 UI 依然重绘了! 因为监听器是绑定在 counter 上的,它不知道你的逻辑其实只关心结果。

在简单的计数器里这没问题。但如果是一个包含几百个列表项的页面,这种“无效刷新”会积少成多,最终拖慢你的帧率。

2. LiveCompute:结果导向的“守门员”

为了解决这个问题,live_states 引入了 LiveCompute。它在原始数据和 UI 之间建立了一道“防火墙”。

late final isEven = LiveCompute<bool>(owner, () => counter.value % 2 == 0);

LiveCompute 的魔力在于:

  1. 自动依赖追踪:它能感知到自己依赖于 counter
  2. 结果比对:当 counter 变化时,它会重新运行计算逻辑。
  3. 智能拦截:如果计算结果(从 truetrue)没变,它会拦截掉通知,不让刷新信号触达 UI。

这样,UI 就像拥有了“手术刀级”的觉察力:只有当偶数变成奇数的那一刻,它才会动。

3. 进阶实战:解决“搜索过滤”的重灾区

搜索过滤是性能问题的重灾区。假设你有一个 1000 行的商品列表,每次用户敲一个字符,你都要过滤并重绘。

如果不使用 LiveCompute,只要搜索词变了,即便过滤后的列表内容完全一样,UI 也会进行昂贵的 ListView Diff。

class ShoppingVM extends LiveViewModel {
  late final searchKey = LiveData<String>('', owner);
  late final allItems = LiveData<List<Product>>([], owner);

  // 使用 LiveCompute 缓存过滤结果
  late final filteredList = LiveCompute<List<Product>>(owner, () {
    if (searchKey.value.isEmpty) return allItems.value;
    return allItems.value.where((p) => p.name.contains(searchKey.value)).toList();
  });
}

在这里,filteredList 不仅自动监听了 searchKeyallItems,它还充当了缓存层

  • 当用户输入一个字符,但并没有改变过滤出的结果数量和顺序时(比如输入一个空格),LiveCompute 内部通过 verifyDataChange(默认开启)发现 List 的引用或内容没变。
  • 于是,整棵 ListView 被保护了起来,完全没有发生重绘。
4. 底层逻辑:它是如何“知道”依赖变化的?

这就要联动到上一篇讲的 Zone 机制了。

LiveCompute 运行它的计算函数时,它也会开辟一个 Zone 气泡。在气泡里访问的所有 LiveData 都会自动建立与该 LiveCompute 的绑定关系。

这形成了一个美妙的链式结构: 原始数据 -> LiveCompute (拦截器) -> LiveScope (UI 节点)

这个链条是完全声明式的。你不需要手动写任何 addListener。你只需要定义规则,性能优化就会像自来水一样,在底层自动流淌。

5. 给开发者的建议:什么时候该用 LiveCompute?

并不是所有的派生状态都要用 LiveCompute。如果你的计算逻辑极其简单(比如字符串拼接),且该状态变动频繁,那么普通的 getter 依然是首选。

但如果满足以下任何一点,请务必使用 LiveCompute

  1. 计算逻辑较重(涉及循环、大量对象创建)。
  2. 结果变动频率远低于输入变动频率(如:价格超过 100 元才显示、输入满 11 位才显示手机号图标)。
  3. 结果是一个复杂的对象/列表(利用其缓存机制避免 UI 层重复计算)。
结语

在响应式编程的世界里,**“不该动的时候不动”**比“该动的时候能动”更难做到。

LiveCompute 的设计初衷,就是想让开发者在追求业务逻辑清晰的同时,不再需要为性能担惊受怕。它把“派生状态”从一个潜在的性能炸弹,变成了一个优雅的、可拦截的精准信号源。


下一篇预告: 《【LiveStates 04】不仅是状态管理:解锁 Recoverable/Refreshable 工业级特性》。