前言
对于非顶级的 Store
,我们测试的时候会发现一个有趣的现象,那就是 StoreConnector
构建的 Widget
在状态发生改变的时候,并不会重建整个子组件,而是只更新依赖于 converter
转换后对象的组件。这说明 StoreConnector
能够精准地定位到哪个子组件依赖状态变量,从而实现精准刷新,提高效率。这和 Provider
的 select
方法类似。
本篇我们就来分析一下 StoreConnector
的源码,看一下是如何实现精准刷新的。
验证
我们先看一个示例,来验证一下我们上面的说法,话不多说,先看测试代码。我们定义了两个按钮,一个点赞,一个收藏,每次点击调度对应的 Action 使得对应的数量加1。两个按钮的实现基本类似,只是依赖状态的数据不同。
class DynamicDetailWrapper extends StatelessWidget {
final store = Store<PartialRefreshState>(
partialRefreshReducer,
initialState: PartialRefreshState(favorCount: 0, praiseCount: 0),
);
DynamicDetailWrapper({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
print('build');
return StoreProvider<PartialRefreshState>(
store: store,
child: Scaffold(
appBar: AppBar(
title: Text('局部 Store'),
),
body: Stack(
children: [
Container(height: 300, color: Colors.red),
Positioned(
bottom: 0,
height: 60,
width: MediaQuery.of(context).size.width,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_PraiseButton(),
_FavorButton(),
],
))
],
),
));
}
}
class _FavorButton extends StatelessWidget {
const _FavorButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
print('FavorButton');
return StoreConnector<PartialRefreshState, int>(
builder: (context, count) => Container(
alignment: Alignment.center,
color: Colors.blue,
child: TextButton(
onPressed: () {
StoreProvider.of<PartialRefreshState>(context)
.dispatch(FavorAction());
},
child: Text(
'收藏 $count',
style: TextStyle(color: Colors.white),
),
style: ButtonStyle(
minimumSize: MaterialStateProperty.resolveWith((states) =>
Size((MediaQuery.of(context).size.width / 2), 60))),
),
),
converter: (store) => store.state.favorCount,
distinct: true,
);
}
}
class _PraiseButton extends StatelessWidget {
const _PraiseButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
print('PraiseButton');
return StoreConnector<PartialRefreshState, int>(
builder: (context, count) => Container(
alignment: Alignment.center,
color: Colors.green[400],
child: TextButton(
onPressed: () {
StoreProvider.of<PartialRefreshState>(context)
.dispatch(PraiseAction());
},
child: Text(
'点赞 $count',
style: TextStyle(color: Colors.white),
),
style: ButtonStyle(
minimumSize: MaterialStateProperty.resolveWith((states) =>
Size((MediaQuery.of(context).size.width / 2), 60))),
),
),
converter: (store) => store.state.praiseCount,
distinct: false,
);
}
}
按正常的情况,状态更新后应该是整个子组件rebuild
,但是实际运行我们发现只有依赖于状态变量的TextButton
和其子组件 Text
进行了 rebuild
。我们在两个按钮的 build
方法打印了对应的信息,然后在 TextButton
(build
方法在其父类ButtonStyleButton
中)和 Text
组件的 build
中打上断点,来看一下运行效果。
从运行结果看,点击按钮的时候 TextButton
和 Text
的 build
方法均被调用了,但是 FavorButton
和 PraiseButton
的 build
方法并没有调用(未打印对应的信息)。这说明 StoreConnector
进行了精准的局部更新。接下来我们从源码看看是怎么回事?
StoreConnector 源码分析
StoreConnector
的源码很简单,本身 StoreConnector
继承自 StatelessWidget
,除了定义的构造方法和属性(均为 final)外,就是一个 build
方法,只是 build方法比较特殊,返回的是一个_StoreStreamListener<S, ViewModel>
组件。来看看这个组件是怎么回事。
@override
Widget build(BuildContext context) {
return _StoreStreamListener<S, ViewModel>(
store: StoreProvider.of<S>(context),
builder: builder,
converter: converter,
distinct: distinct,
onInit: onInit,
onDispose: onDispose,
rebuildOnChange: rebuildOnChange,
ignoreChange: ignoreChange,
onWillChange: onWillChange,
onDidChange: onDidChange,
onInitialBuild: onInitialBuild,
);
}
_StoreStreamListener
是一个StatefulWidget
,核心实现在_StoreStreamListenerState<S, ViewModel>
中,代码如下所示。
class _StoreStreamListenerState<S, ViewModel>
extends State<_StoreStreamListener<S, ViewModel>> {
late Stream<ViewModel> _stream;
ViewModel? _latestValue;
ConverterError? _latestError;
// `_latestValue!` would throw _CastError if `ViewModel` is nullable,
// therefore `_latestValue as ViewModel` is used.
// https://dart.dev/null-safety/understanding-null-safety#nullability-and-generics
ViewModel get _requireLatestValue => _latestValue as ViewModel;
@override
void initState() {
widget.onInit?.call(widget.store);
_computeLatestValue();
if (widget.onInitialBuild != null) {
WidgetsBinding.instance?.addPostFrameCallback((_) {
widget.onInitialBuild!(_requireLatestValue);
});
}
_createStream();
super.initState();
}
@override
void dispose() {
widget.onDispose?.call(widget.store);
super.dispose();
}
@override
void didUpdateWidget(_StoreStreamListener<S, ViewModel> oldWidget) {
_computeLatestValue();
if (widget.store != oldWidget.store) {
_createStream();
}
super.didUpdateWidget(oldWidget);
}
void _computeLatestValue() {
try {
_latestError = null;
_latestValue = widget.converter(widget.store);
} catch (e, s) {
_latestValue = null;
_latestError = ConverterError(e, s);
}
}
@override
Widget build(BuildContext context) {
return widget.rebuildOnChange
? StreamBuilder<ViewModel>(
stream: _stream,
builder: (context, snapshot) {
if (_latestError != null) throw _latestError!;
return widget.builder(
context,
_requireLatestValue,
);
},
)
: _latestError != null
? throw _latestError!
: widget.builder(context, _requireLatestValue);
}
ViewModel _mapConverter(S state) {
return widget.converter(widget.store);
}
bool _whereDistinct(ViewModel vm) {
if (widget.distinct) {
return vm != _latestValue;
}
return true;
}
bool _ignoreChange(S state) {
if (widget.ignoreChange != null) {
return !widget.ignoreChange!(widget.store.state);
}
return true;
}
void _createStream() {
_stream = widget.store.onChange
.where(_ignoreChange)
.map(_mapConverter)
// Don't use `Stream.distinct` because it cannot capture the initial
// ViewModel produced by the `converter`.
.where(_whereDistinct)
// After each ViewModel is emitted from the Stream, we update the
// latestValue. Important: This must be done after all other optional
// transformations, such as ignoreChange.
.transform(StreamTransformer.fromHandlers(
handleData: _handleChange,
handleError: _handleError,
));
}
void _handleChange(ViewModel vm, EventSink<ViewModel> sink) {
_latestError = null;
widget.onWillChange?.call(_latestValue, vm);
final previousValue = vm;
_latestValue = vm;
if (widget.onDidChange != null) {
WidgetsBinding.instance?.addPostFrameCallback((_) {
if (mounted) {
widget.onDidChange!(previousValue, _requireLatestValue);
}
});
}
sink.add(vm);
}
void _handleError(
Object error,
StackTrace stackTrace,
EventSink<ViewModel> sink,
) {
_latestValue = null;
_latestError = ConverterError(error, stackTrace);
sink.addError(error, stackTrace);
}
}
关键的设置都在 initState 方法中。在 initState 方法中,如果设置了 onInit
方法,就会将 store
传递过去调用状态的初始化方法,例如下面就是我们在购物清单应用中对 onInit
属性的使用。
onInit: (store) => store.dispatch(ReadOfflineAction()),
接下来是调用_computeLatestValue
方法,实际是通过converter
方法获得转换后的ViewModel
对象的值,这个值存储在ViewModel _latestValue
属性中。然后是,如果定义了 onInitialBuild
方法,就会使用 ViewModel
的值做初始化构造。
最后调用了_createStream
方法,这个方法很关键!!!实际上就是吧 Store
的onChange
事件按照一定的过滤方式转变了成了Stream<ViewModel>
对象,其实相当于是只监听了 Store
中经过 converter
方法转换后那一部分ViewModel
对象的变化——也就是实现了局部监听。处理数据变化的方法为_handleChange
。实际上就是将变化后的 ViewModel
加入到流中:
sink.add(vm);
因为 build
方法中使用的是 StremaBuilder
组件,并且会监听_stream
对象,因此当状态数据转换后的 ViewModel
对象发生改变时,会触发 build
方法进行重建。而这个方法最终会调用 StoreConnector
中的 builder
属性对应的方法。这部分代码正好是 PraiseButton
或 FavorButton
的下级组件,这就是为什么状态发生变化时 PraiseButton
和 FavorButton
不会被重建的原因,因为他们不是StoreConnector
的下级组件,而是上级组件。
也就是说, 使用StoreConnector
这种方式时,当状态发生改变后,之后刷新它的下级组件。因此,从性能考虑,我们可以做最小范围的包裹,比如这个例子,我们可以只包裹 Text
组件,这样 Container
和 TextButton
也不会被刷新了。
为了对比,我们只修改了 PraiseButton
的代码,实际打断点发现点击点赞按钮的Container
不会被刷新,而TextButton
会刷新,分析发现是TextButton
的外观样式在点击的时候改变导致的,并不是Store
状态改变导致。也就是说,通过最小范围使用 StoreConnector
包裹子组件,我们可以将刷新的范围缩到最小,从而最大限度地提升性能。具体代码可以到这里下载(partial_refresh
部分):Redux 状态管理代码。
class _PraiseButton extends StatelessWidget {
const _PraiseButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
print('PraiseButton');
return Container(
alignment: Alignment.center,
color: Colors.green[400],
child: TextButton(
onPressed: () {
StoreProvider.of<PartialRefreshState>(context)
.dispatch(PraiseAction());
},
child: StoreConnector<PartialRefreshState, int>(
builder: (context, count) => Text(
'点赞 $count',
style: TextStyle(color: Colors.white),
),
converter: (store) => store.state.praiseCount,
distinct: false,
),
style: ButtonStyle(
minimumSize: MaterialStateProperty.resolveWith(
(states) => Size((MediaQuery.of(context).size.width / 2), 60))),
),
);
}
}
总结
很多时候我们在使用第三方插件的时候,都是跑跑 demo,然后直接上手就用。确实,这样也能够达到功能实现的目的,但是如果真的遇到性能上面的问题的时候,往往不知所措。因此,对于有些第三方插件,还是有必要保持好奇心,了解其中的实现机制,做到知其然知其所以然。
网友评论