Flutter 入門與實戰(六十四):這篇很長,為了效能,你忍一下 —— 從原始碼看 flutter_redux精準重新整理

島上碼農發表於2021-08-26

這是我參與8月更文挑戰的第27天,活動詳情檢視:8月更文挑戰

前言

對於非頂級的 Store,我們測試的時候會發現一個有趣的現象,那就是 StoreConnector 構建的 Widget 在狀態發生改變的時候,並不會重建整個子元件,而是隻更新依賴於 converter 轉換後物件的元件。這說明 StoreConnector 能夠精準地定位到哪個子元件依賴狀態變數,從而實現精準重新整理,提高效率。這和 Providerselect 方法類似。 本篇我們就來分析一下 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 方法列印了對應的資訊,然後在 TextButtonbuild 方法在其父類ButtonStyleButton中)和 Text 元件的 build 中打上斷點,來看一下執行效果。

螢幕錄製2021-08-26 下午8.47.54.gif

從執行結果看,點選按鈕的時候 TextButtonTextbuild 方法均被呼叫了,但是 FavorButtonPraiseButtonbuild 方法並沒有呼叫(未列印對應的資訊)。這說明 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 方法,這個方法很關鍵!!!實際上就是吧 StoreonChange 事件按照一定的過濾方式轉變了成了 Stream<ViewModel>物件,其實相當於是隻監聽了 Store 中經過 converter 方法轉換後那一部分ViewModel 物件的變化——也就是實現了區域性監聽。處理資料變化的方法為_handleChange。實際上就是將變化後的 ViewModel 加入到流中:

sink.add(vm);
複製程式碼

因為 build 方法中使用的是 StremaBuilder 元件,並且會監聽_stream 物件,因此當狀態資料轉換後的 ViewModel 物件發生改變時,會觸發 build 方法進行重建。而這個方法最終會呼叫 StoreConnector 中的 builder 屬性對應的方法。這部分程式碼正好是 PraiseButtonFavorButton 的下級元件,這就是為什麼狀態發生變化時 PraiseButtonFavorButton不會被重建的原因,因為他們不是StoreConnector 的下級元件,而是上級元件。

也就是說, 使用StoreConnector這種方式時,當狀態發生改變後,之後重新整理它的下級元件。因此,從效能考慮,我們可以做最小範圍的包裹,比如這個例子,我們可以只包裹 Text 元件,這樣 ContainerTextButton 也不會被重新整理了。

為了對比,我們只修改了 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,然後直接上手就用。確實,這樣也能夠達到功能實現的目的,但是如果真的遇到效能上面的問題的時候,往往不知所措。因此,對於有些第三方外掛,還是有必要保持好奇心,瞭解其中的實現機制,做到知其然知其所以然

相關文章