精打细算:如何减少不必要的 Widget 重建
一句话总结
学习如何使用
select来精确监听你所关心的数据,避免因无关数据变化导致的额外重建,从而优化你的 Riverpod 应用性能。
学完了我们目前介绍的所有内容,你已经可以构建一个功能齐全的应用程序了。但是,你可能会对性能方面产生一些疑问。
在本文中,我们将介绍一些优化代码的小技巧。
警告
在进行任何优化之前,请务必对你的应用程序进行基准测试。为了微不足道的性能提升而增加代码的复杂性,可能得不偿失。
你可能已经注意到,默认情况下,使用 ref.watch 会导致一个问题:只要被监听对象中的任何一个属性发生变化,相关的 Widget 或 Provider 就会被重建。
举个例子,假设你正在监听一个 User 对象,但其实你只用到了它的 name 属性。如果这时 User 的 age 属性变了,你的 Widget 仍然会跟着重建。
但很多时候,我们只关心对象中的一小部分属性,我们希望只有在这些我们真正关心的属性变化时,才触发重建。
这可以通过 Provider 的 select 功能来实现。
当你使用了 select,ref.watch 将不再返回整个对象,而是只返回你“挑选”出来的那些属性。
这样,你的 Widget 或 Provider 就只会在这些被挑选出来的属性发生变化时,才会重建。
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:
@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
}