這是我參與8月更文挑戰的第27天,活動詳情檢視:8月更文挑戰
前言
對於非頂級的 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,然後直接上手就用。確實,這樣也能夠達到功能實現的目的,但是如果真的遇到效能上面的問題的時候,往往不知所措。因此,對於有些第三方外掛,還是有必要保持好奇心,瞭解其中的實現機制,做到知其然知其所以然。