Flutter 入門與實戰(四十五):萬字長文!一文搞懂InheritedWidget 局部刷新機制

這是我參與8月更文挑戰的第8天,活動詳情查看:8月更文挑戰數組

前言

上一篇咱們從源碼角度分析了 setState 的過程,從而瞭解到爲何 setState 方法被調用的時候會從新構建整個 Widget 樹。可是,Widget 樹的從新構建並不意味着渲染元素樹也須要從新構建,事實上渲染樹只是作了更新,而不必定是移除後在渲染。微信

可是,咱們的 ModelBinding類也是使用了 setState 進行狀態更新的,爲何它的子組件沒有從新構建,而只是更新了依賴於狀態的子組件的 build 方法呢?除了使用了內部的 InheritedWidget包裹了子組件外,其餘和普通的 StatefulWidget 沒什麼區別。如前面兩篇分析 從InheritedWidget瞭解狀態管理同樣,差異就是在這個 InheritedWidget上。本着技術人刨根問底的精神,本篇就來看一下 InheritedWidget 在調用 setState的時候究竟有什麼不一樣。markdown

image.png

知其然,知其因此然。在閱讀本篇文章前,若是對 Flutter 的狀態管理不是特別清楚的,建議閱讀前幾篇文章瞭解一下背景:app

InheritedWidget與 StatefulWidget 的區別

首先,InheritedWidgetStatefulWidget 的繼承鏈不一樣,對好比下。 渲染過程-Stateful和 Inherited 對比.png InheritedWidget繼承自 ProxyWidget,以後纔是 Widget,而 StatefulWidget 直接繼承 Widget。 其二是建立的渲染元素類不一樣,InheritedWidgetcreateElement 返回的是InheritedElement,而 StatefulWidgetcreateElement 返回的是StatefulElementless

咱們在上一篇已經知道,實際的渲染控制是有 Element 類來完成的,實際上WidgetcreateElement 方法就是將 Widget 對象傳給 Element 對象,由 Element 對象根據 Widget 的組件配置來決定如何渲染。ide

InhretiedWidget 的定義很簡單,以下所示:源碼分析

abstract class InheritedWidget extends ProxyWidget {
  const InheritedWidget({Key? key, required Widget child})
      : super(key: key, child: child);

  @override
  InheritedElement createElement() => InheritedElement(this);

  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
}
複製代碼

updateShouldNotify方法用於 InheritedWidget 的子類實現,已決定是否通知其子組件(widget)。例如,若是數據沒有發生改變(典型的以下拉刷新沒有新的數據),那麼就能夠返回 false,從而無需更新子組件,減小性能消耗。以前咱們的 ModelBinding 例子中是直接返回了 true,也就是每次發生變化都會通知子組件。接下來就看 InheritedElementStatefulElement 的區別了。post

InheritedElement 與 StatefulElement 的區別

上一篇咱們已經分析過 StatefulElement 了,他在 setState 後會調用重建方法 performRebuildperformRebuild 方法在父類Component 中實現的。核心是當 Widget 樹發生改變後,根據新的 Widget 樹調用 updateChild 方法來更新子元素。性能

而上一篇的 ModelBinding 調用 setState 的時候,由於它自身是一個 StatefulWidget,毫無疑問它也會調用到 updateChild來更新子元素。從執行結果來看,因爲 ModelBinding 的例子中沒有出現從新構建 Widget 樹的狀況,所以應該是在 updateChild 前的處理不一樣。 在 updateChild 以前會調用組件的 build 方法來獲取新的 Widget 樹。是這裏不一樣嗎?繼續往下看。字體

渲染過程-InheritedElement 與 StatefulElement.png

InheritedWidget 對應,InheritedElement上面還多了一層繼承,那就是 ProxyElement。而偏偏在 ProxyElement 咱們找到了build 方法。與 StatefulElement不一樣,這裏的 build 方法沒有調用對應 Widget 對象的 build 方法,而是直接返回了 widget.child

// ProxyElement的 build 方法
@override
Widget build() => widget.child;

// StatefulElement 的 build 方法
@override
Widget build() => state.build(this);

// StatelessElement 的 build方法
@override
Widget build() => widget.build(this);

複製代碼

由此咱們就知道了爲何 InheritedWidget在狀態更新的時候爲何沒有從新構建其子組件樹了,這是由於在ProxyElement中直接就返回了已經構建的子組件樹,而不是重建。你是否是覺得真相大白了?說好的刨根問底呢?難道咱們不該該問問若是子組件樹發生了改變,ProxyElement 是如何感知的?好比插入了一個新的元素,或者某個元素的渲染參數變了(顏色,字體,內容等),渲染層是怎麼知道的?繼續繼續! image.png

InheritedElement如何感知組件樹的變化

先看一下 InheritedElement 的類結構。

classDiagram
    Element <-- ComponentElement
    ComponentElement <-- ProxyElement
    ProxyElement <-- InheritedElement

    class Element {
        -dependOnInheritedWidgetOfExactType()
        -dependOnInheritedElement()
    }

    class InheritedElement {
    -Map<Element, Object?> _dependents
        -void _updateInheritance()
        -getDependencies(Element dependent)
        setDependencies(Element dependent, Object? value)
        updateDependencies(Element dependent, Object? aspect)
        notifyDependent(covariant InheritedWidget oldWidget, Element dependent)
        updated(InheritedWidget oldWidget)
        notifyClients(InheritedWidget oldWidget)

    }

    class ProxyElement {
        -build()
        -update(ProxyWidget newWidget)
        -updated(covariant ProxyWidget oldWidget)
        -notifyClients(covariant ProxyWidget oldWidget)
}

從類結構上看也不復雜,這是由於大部分渲染的管理已經在父類的 ComponentElementElement 中完成了。build 方法咱們已經講過了,重點來看一下在 InheritedWidget 的父組件調用 setState 後的過程。 咱們在子組件須要獲取狀態管理的時候,使用的方法是:

ModelBindingV2.of<FaceEmotion>(context)
複製代碼

這個方法實際調用的是:

_ModelBindingScope<T> scope =
  context.dependOnInheritedWidgetOfExactType(aspect: _ModelBindingScope);
複製代碼

這裏的dependOnInheritedWidgetOfExactType方法在 BuildContext定義,但其實是Element 實現。這裏會訪問一個HashMap 對象_inheritedWidgets,從數組中找到對應類型的InheritedElement

@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor,
    {Object? aspect}) {
  assert(ancestor != null);
  _dependencies ??= HashSet<InheritedElement>();
  _dependencies!.add(ancestor);
  ancestor.updateDependencies(this, aspect);
  return ancestor.widget;
}

@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>(
    {Object? aspect}) {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement? ancestor =
      _inheritedWidgets == null ? null : _inheritedWidgets![T];
  if (ancestor != null) {
    assert(ancestor is InheritedElement);
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}
複製代碼

這個數組其實是在 mount 方法中調用_updateInheritance 中完成初始化的。而在InheritedElement 中重載了 Element 的這個方法。也就是在建立 InheritedWidget 的時候,在 mount 中就將 InheritedElement 與對應的組件運行時類型進行了關聯。

@override
void _updateInheritance() {
  assert(_lifecycleState == _ElementLifecycle.active);
  final Map<Type, InheritedElement>? incomingWidgets =
      _parent?._inheritedWidgets;
  if (incomingWidgets != null)
    _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
  else
    _inheritedWidgets = HashMap<Type, InheritedElement>();
  _inheritedWidgets![widget.runtimeType] = this;
}
複製代碼

首先這個方法會將父級的所有 InheritedWidgets延續下來,而後在將本身(InheritedElement)存入到這個 HashMap中,以便後續可以找到該元素。

所以,當在子組件中使用dependOnInheritedWidgetOfExactType的時候,實際上執行的是 dependOnInheritedElement 方法,傳遞的參數是經過類型找到的 InheritedElement 元素和指定的 InheritedWidget 類型參數 aspect,這裏就是咱們的_ModeBindScope<T>,而後會將當前的渲染元素(Element 子類)與其綁定,告知 InheritedElement對象這個組件會依賴於它的InheritedWidget。咱們從調試的結果能夠看到,在_dependents 中存在了這麼一個對象。就這樣,InheritedElement 就和組件對應的渲染元素創建了聯繫。

image.png

接下來就是看 setState 後,怎麼獲取新的組件樹和更新組件了。咱們已經知道了setState 的時候會調用 performRebuild 方法,在 performRebuild 中會調用 ElementupdateChild 方法,如今來看InheritedElementupdateChild 作了什麼事情。實際上 updateChild 會調用 child.update(newWidget)方法:

else if (hasSameSuperclass &&
      Widget.canUpdate(child.widget, newWidget)) {
    if (child.slot != newSlot) updateSlotForChild(child, newSlot);
    child.update(newWidget);
    //...
    newChild = child;
 }

// ...

return newChild;
複製代碼

而在 ProxyElement 中,重寫了 update 方法。

@override
void update(ProxyWidget newWidget) {
  final ProxyWidget oldWidget = widget;
  assert(widget != null);
  assert(widget != newWidget);
  super.update(newWidget);
  assert(widget == newWidget);
  updated(oldWidget);
  _dirty = true;
  rebuild();
}
複製代碼

這裏的 newWidget 是 setState 的時候構建的新的組件配置,所以和 oldWidget 並不相同。對於 InheritedWidget,它會先調用updated(oldWidget),這個方法實際上就是通知依賴 InheirtedWidget 的組件更新:

@protected
void updated(covariant ProxyWidget oldWidget) {
  notifyClients(oldWidget);
}

// InheritedElement類
@override
void updated(InheritedWidget oldWidget) {
  if (widget.updateShouldNotify(oldWidget)) super.updated(oldWidget);
}

@protected
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
  dependent.didChangeDependencies();
}

@override
void notifyClients(InheritedWidget oldWidget) {
  assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
  for (final Element dependent in _dependents.keys) {
      assert(() {
        // check that it really is our descendant
        Element? ancestor = dependent._parent;
        while (ancestor != this && ancestor != null)
          ancestor = ancestor._parent;
        return ancestor == this;
      }());
      // check that it really depends on us
      assert(dependent._dependencies!.contains(this));
      notifyDependent(oldWidget, dependent);
    }
  }
}
複製代碼

實際上最終調用了依賴 InheritedWidget 組件渲染元素的 didChangeDependencies 方法,咱們在這個方法打印出來看一下。 image.png 在元素的 didChangeDependencies 中就會調用 markNeedsBuild將元素標記爲須要更新,而後後續的過程就和 StatefulElement 的同樣了。而對於沒有依賴狀態的元素,由於沒有在_dependent 中,所以不會被更新。 而 ModelBinding 所在的組件是 StatelessWidget,所以最初的這個 Widget 配置樹一旦建立就不會改變,而子組件樹若是要 改變的話只有兩種狀況: 一、子組件是 StatefulWidget,經過setState 改變,那這不屬於 InheritedWidget 的範疇了,而是經過 StatefulWidget 的更新方式完成——固然,這種作法不推薦。 二、子組件的組件樹改變依賴於狀態嗎,那這個時候天然會在狀態改變的時候更新。

由此,咱們終於弄明白了InheritedWidget的組件樹的感知和通知子組件刷新過程。

總結

從 InheritedWidget 實現組件渲染的過程來看,整個過程分爲下面幾個步驟:

  • mount 階段將組件樹運行時類型與對應的 InheritedElement綁定,存入到 _inheritedWidgets 這個 HashMap 中;
  • 在子組件添加對狀態的依賴的時候,實際上將子組件對應的 Element 元素與InheritedElement(具體的 Element 對象從_inheritedWidgets中獲取)進行了綁定,存入到了_dependents 這個 HashMap 中;
  • 當狀態更新的時候,InheritedElement 直接使用舊的組件配置通知子元素的依賴發生了改變,這是經過調用Element 的 didChangeDependencies 方法完成的。
  • 在Element的didChangeDependencies將元素標記爲須要更新,等待下一幀刷新。
  • 而對於沒有依賴狀態的子組件,則不會被加入到_dependent 中,所以不會被通知刷新,進而提升性能。

狀態管理的原理性文章講了好幾篇了,經過這些文章但願可以達到知其然,知其因此然的目的。實際上,Flutter 的組件渲染的核心就在於如何選擇狀態管理來實現組件的渲染,這個對性能影響很大。接下來咱們將以狀態管理插件的應用方式,講述在實際例子中的應用。


我是島上碼農,微信公衆號同名,這是Flutter 入門與實戰的專欄文章。

👍🏻:以爲有收穫請點個贊鼓勵一下!

🌟:收藏文章,方便回看哦!

💬:評論交流,互相進步!

相關文章
相關標籤/搜索