Flutter局部刷新原理

  • 概述

    在Flutter中,我們知道,刷新界面要調(diào)用setState方法,在一個(gè)界面中,通常只需要刷新某個(gè)組件或者某一部分組件,這種情況下調(diào)用父級(jí)State的setState方法會(huì)造成不必要的資源浪費(fèi)。 在這種需求下,我們需要找到一個(gè)方式可以進(jìn)行局部刷新。

  • 做法和原理

    其實(shí)局部刷新很簡(jiǎn)單,我們只需要把需要刷新的組件聚到一個(gè)StatefulWidget中,通過一個(gè)State來管理,然后刷新的時(shí)候調(diào)用這個(gè)State的setState方法即可完成針對(duì)這部分組件的刷新,父級(jí)和兄弟級(jí)的StatefulWidget都不會(huì)被引起rebuild。

    原理也很好理解,就是調(diào)用setState的流程,調(diào)用setState方法:

    @protected
    void setState(VoidCallback fn) {
      ...
      _element!.markNeedsBuild();
    }
    

    Element的markNeedsBuild方法會(huì)調(diào)用BuildOwner的scheduleBuildFor方法:

    void markNeedsBuild() {
      ...
      if (dirty)
        return;
      _dirty = true;
      owner!.scheduleBuildFor(this);
    }
    

    scheduleBuildFor方法會(huì)把當(dāng)前element放入BuildOwner的_dirtyElements中:

    void scheduleBuildFor(Element element) {
      ...
      _dirtyElements.add(element);
      element._inDirtyList = true;
      ...
    }
    

    當(dāng)下一個(gè)Frame到來時(shí)框架會(huì)調(diào)用WidgetsBinding的drawFrame方法:

    @override
    void drawFrame() {
      ...
      try {
        if (renderViewElement != null)
          buildOwner!.buildScope(renderViewElement!);
        //這里面是布局、合成層信息、繪制等流程
        super.drawFrame();
        buildOwner!.finalizeTree();
      } finally {
          ...
      }
      ...
    }
    

    這里會(huì)調(diào)用BuildOwner的buildScope方法:

    @pragma('vm:notify-debugger-on-exception')
    void buildScope(Element context, [ VoidCallback? callback ]) {
      ...
      try {
        ...
        _dirtyElements.sort(Element._sort);
          ...
        int dirtyCount = _dirtyElements.length;
        int index = 0;
        while (index < dirtyCount) {
          ...
          try {
            //重新構(gòu)建
            _dirtyElements[index].rebuild();
          } catch (e, stack) {
            ...
          }
          index += 1;
          ...
        }
          ...
      } finally {
        for (final Element element in _dirtyElements) {
          assert(element._inDirtyList);
          element._inDirtyList = false;
        }
        //清空_dirtyElements
        _dirtyElements.clear();
        ...
      }
      ...
    }
    

    在buildScope方法中會(huì)循環(huán) _dirtyElements,依次調(diào)用里面的element的rebuild方法進(jìn)行構(gòu)建,rebuild方法中又會(huì)調(diào)用performRebuild方法:

    @pragma('vm:prefer-inline')
    void rebuild() {
      ...
      performRebuild();
      ...
    }
    

    performRebuild方法是在StatefulElement和StatelessElement的共同父類ComponentElement中實(shí)現(xiàn)的,在這個(gè)方法中會(huì)調(diào)用build方法創(chuàng)建Widget:

    //StatefulElement中實(shí)現(xiàn)的build方法,可見會(huì)通過state的build方法生成
    @override
    Widget build() => state.build(this);
    //StatelessElement中實(shí)現(xiàn)的build方法
    @override
    Widget build() => widget.build(this);
    

    所以局部刷新原理的核心就是把需要刷新的區(qū)域收到一個(gè)State中,然后調(diào)用這個(gè)State的setState方法就會(huì)使當(dāng)前的這個(gè)State的element變?yōu)閐irty,把它放入需要重新構(gòu)建的element集合中,在幀回調(diào)后會(huì)循環(huán)這個(gè)集合調(diào)用它的rebuild方法進(jìn)行重新構(gòu)建,因?yàn)槲覀兏弦患?jí)的State并沒有執(zhí)行它的setState方法所以不會(huì)添加在需要重新構(gòu)建的element集合中。

  • 關(guān)于get框架的應(yīng)用

    get框架的局部刷新也是通過上面的原理完成的,下面我們來看看他是怎么封裝的。

    首先它使用一個(gè)叫做GetxController的東西來提供統(tǒng)一刷新的api接口:

    abstract class GetxController extends DisposableInterface
        with ListenableMixin, ListNotifierMixin {
      void update([List<Object>? ids, bool condition = true]) {
        if (!condition) {
          return;
        }
        //全部刷新
        if (ids == null) {
          refresh();
        } else {
          //局部刷新
          for (final id in ids) {
            refreshGroup(id);
          }
        }
      }
    }
    

    在頁面打開的時(shí)候會(huì)創(chuàng)建這個(gè)controller,然后通過調(diào)用這個(gè)controller的update方法執(zhí)行局部構(gòu)建,可以看到,局部構(gòu)建需要一個(gè)id,這個(gè)id是什么時(shí)候綁定的呢?

    使用get框架的局部刷新需要把要刷新的組件們用一個(gè)GetBuilder包裝起來,那這個(gè)GetBuilder構(gòu)造時(shí)就可以傳入一個(gè)id值,GetBuilder是一個(gè)StatefulWidget,他的State中的initState方法里調(diào)用了一個(gè)_subscribeToController方法:

    void _subscribeToController() {
      _remove?.call();
      _remove = (widget.id == null)
          //全部刷新的回調(diào)添加
          ? controller?.addListener(
              _filter != null ? _filterUpdate : getUpdate,
            )
          //局部刷新的回調(diào)添加
          : controller?.addListenerId(
              widget.id,
              _filter != null ? _filterUpdate : getUpdate,
            );
    }
    

    addListenerId方法中:

    Disposer addListenerId(Object? key, GetStateUpdate listener) {
      _updatersGroupIds![key] ??= <GetStateUpdate>[];
      _updatersGroupIds![key]!.add(listener);
      return () => _updatersGroupIds![key]!.remove(listener);
    }
    

    可以看到,這里根據(jù)id添加了一個(gè)回調(diào)函數(shù),這里用的數(shù)組存放,可見可以通過指定同一個(gè)id的方式來實(shí)現(xiàn)幾個(gè)區(qū)域聯(lián)動(dòng)刷新。

    回到上面的refreshGroup方法,內(nèi)部會(huì)調(diào)用_notifyIdUpdate方法:

    void _notifyIdUpdate(Object id) {
      if (_updatersGroupIds!.containsKey(id)) {
        final listGroup = _updatersGroupIds![id]!;
        for (var item in listGroup) {
          item();
        }
      }
    }
    

    可見,在這里根據(jù)id查找并執(zhí)行了所有相關(guān)的函數(shù)回調(diào)。

    那么函數(shù)回調(diào)是什么呢?_subscribeToController方法中,addListenerId方法添加的函數(shù)回調(diào)如果默認(rèn)的話是getUpdate,它指向一個(gè)函數(shù),這個(gè)函數(shù)在GetBuilderState依賴的mixin—GetStateUpdaterMixin中定義:

    void getUpdate() {
      if (mounted) setState(() {});
    }
    

    可以看到,正是在這里調(diào)用了setState來觸發(fā)重新構(gòu)建的,因?yàn)槭窃贕etBuilderState中調(diào)用的setState方法,所以在GetBuilder之上的其他State是不會(huì)觸發(fā)回調(diào)的,這和上面我們分析的原理是一樣的。

  • 總結(jié)

    局部構(gòu)建的原理就是用子State來攔截構(gòu)建的范圍,不把所有的組件樹都放在一個(gè)大的State里面構(gòu)建,通過調(diào)用子State的setState方法來實(shí)現(xiàn)針對(duì)子Widget樹的重新構(gòu)建,這樣就實(shí)現(xiàn)了局部刷新。

    據(jù)此,我們當(dāng)然可以不用get框架的局部刷新,完全可以自定義,我試著寫了一下,有幾個(gè)需要注意的點(diǎn):

    1. setState一定要在需要局部刷新的State中調(diào)用;

    2. 調(diào)用setState的邏輯要通過一個(gè)函數(shù)暴露出來;

    3. 因?yàn)槲覀円WC隨時(shí)可以刷新,所以我們需要一個(gè)隨時(shí)獲取且不會(huì)改變的對(duì)象來保存這個(gè)回調(diào),相當(dāng)于get的controller;

    4. 因?yàn)槲覀兛赡軙?huì)有很多個(gè)局部需要刷新,它們必須獨(dú)立且可以區(qū)分,所以我們需要保存回調(diào)函數(shù)的集合是一個(gè)可已按照key-value的形式來存放的集合,get中使用了String-List的形式,這樣可以刷新好幾塊區(qū)域,我用的是一個(gè)String-dynamic的Map來存放,這個(gè)過程中發(fā)現(xiàn)了一個(gè)需要注意的點(diǎn):

      Map的putIfAbsent方法的第二個(gè)參數(shù)規(guī)定是:

      V putIfAbsent(K key, V ifAbsent());
      

      如果使用這個(gè)方法設(shè)置回調(diào)函數(shù),則需要在一個(gè)函數(shù)中返回這個(gè)回調(diào)函數(shù)才行。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容