Skip to content

精打细算:如何减少不必要的 Widget 重建

一句话总结

学习如何使用 select 来精确监听你所关心的数据,避免因无关数据变化导致的额外重建,从而优化你的 Riverpod 应用性能。


学完了我们目前介绍的所有内容,你已经可以构建一个功能齐全的应用程序了。但是,你可能会对性能方面产生一些疑问。

在本文中,我们将介绍一些优化代码的小技巧。

警告

在进行任何优化之前,请务必对你的应用程序进行基准测试。为了微不足道的性能提升而增加代码的复杂性,可能得不偿失。

你可能已经注意到,默认情况下,使用 ref.watch 会导致一个问题:只要被监听对象中的任何一个属性发生变化,相关的 Widget 或 Provider 就会被重建。

举个例子,假设你正在监听一个 User 对象,但其实你只用到了它的 name 属性。如果这时 Userage 属性变了,你的 Widget 仍然会跟着重建。

但很多时候,我们只关心对象中的一小部分属性,我们希望只有在这些我们真正关心的属性变化时,才触发重建。

这可以通过 Provider 的 select 功能来实现。

当你使用了 selectref.watch 将不再返回整个对象,而是只返回你“挑选”出来的那些属性。

这样,你的 Widget 或 Provider 就只会在这些被挑选出来的属性发生变化时,才会重建。

dart
class User {
  late String firstName, lastName;
}

@riverpod
User example(Ref ref) => User()
  ..firstName = 'John'
  ..lastName = 'Doe';

class ConsumerExample extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 不再像这样写:
    // String name = ref.watch(provider).firstName!;
    // 我们可以这样写:
    String name = ref.watch(exampleProvider.select((it) => it.firstName));
    // 这将导致该 Widget 只监听 "firstName" 的变化。

    return Text('Hello $name');
  }
}

信息

你可以根据需要多次调用 select。对于你想要的每个属性,都可以自由地调用一次。

警告

被挑选出来的属性应该是不可变的。如果你返回一个 List 然后修改了这个列表,是不会触发重建的。

警告

使用 select 会轻微降低单次读取操作的速度,并稍微增加代码的复杂性。

如果那些“其他属性”很少发生变化,那么使用 select 可能并不划算。

挑选异步数据中的属性

如果你试图优化的场景是:一个 Provider 正在监听另一个异步的 Provider,情况会稍微复杂一些。

通常,你会用 ref.watch(anotherProvider.future) 来获取异步数据的值。

问题在于,select 是作用在 AsyncValue 对象上的,而不是直接作用在数据上——而 AsyncValue 是不能被 await 的。

为了解决这个问题,你可以改用 selectAsync。这是专门为异步代码设计的,它能让你在 Provider 发出的数据上执行 select 操作。

它的用法和 select 类似,但它返回的是一个 Future

dart
@riverpod
Object? example(Ref ref) async {
  // 等待一个 user 数据可用,并且只监听 "firstName" 属性
  final firstName = await ref.watch(
    userProvider.selectAsync((it) => it.firstName),
  );

  // TODO use "firstName" to fetch something else
}