這是我參與8月更文挑戰的第8天,活動詳情檢視:8月更文挑戰
前言
上一篇我們從原始碼角度分析了 setState 的過程,從而瞭解到為什麼 setState 方法被呼叫的時候會重新構建整個 Widget 樹。但是,Widget 樹的重新構建並不意味著渲染元素樹也需要重新構建,事實上渲染樹只是做了更新,而不一定是移除後在渲染。
但是,我們的 ModelBinding類也是使用了 setState 進行狀態更新的,為什麼它的子元件沒有重新構建,而只是更新了依賴於狀態的子元件的 build 方法呢?除了使用了內部的 InheritedWidget包裹了子元件外,其他和普通的 StatefulWidget 沒什麼區別。如前面兩篇分析 從InheritedWidget瞭解狀態管理一樣,差別就是在這個 InheritedWidget上。本著技術人刨根問底的精神,本篇就來看一下 InheritedWidget 在呼叫 setState的時候究竟有什麼不同。
知其然,知其所以然。在閱讀本篇文章前,如果對 Flutter 的狀態管理不是特別清楚的,建議閱讀前幾篇文章瞭解一下背景:
- Flutter 入門與實戰(三十八):從相親來看有狀態和無狀態元件
- Flutter 入門與實戰(三十九):渲染模式詳解
- Flutter 入門與實戰(四十):以購物車為例初探狀態管理
- Flutter入門與實戰(四十一): 從InheritedWidget深入瞭解狀態管理機制(上)
- Flutter入門與實戰(四十二): 從InheritedWidget深入瞭解狀態管理機制(下)
- Flutter 入門與實戰(四十三):setState 和 ModelBinding用法對比
- Flutter入門與實戰(四十四):從原始碼分析setState 的時候到底發生了什麼?
InheritedWidget與 StatefulWidget 的區別
首先,InheritedWidget
和 StatefulWidget
的繼承鏈不同,對比如下。
InheritedWidget
繼承自 ProxyWidget
,之後才是 Widget
,而 StatefulWidget
直接繼承 Widget
。
其二是建立的渲染元素類不同,InheritedWidget
的 createElement
返回的是InheritedElement
,而 StatefulWidget
的 createElement
返回的是StatefulElement
。
我們在上一篇已經知道,實際的渲染控制是有 Element
類來完成的,實際上Widget
的createElement
方法就是將 Widget
物件傳給 Element
物件,由 Element
物件根據 Widget
的元件配置來決定如何渲染。
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
,也就是每次發生變化都會通知子元件。接下來就看 InheritedElement
和 StatefulElement
的區別了。
InheritedElement 與 StatefulElement 的區別
上一篇我們已經分析過 StatefulElement
了,他在 setState
後會呼叫重建方法 performRebuild
。performRebuild
方法在父類Component
中實現的。核心是當 Widget
樹發生改變後,根據新的 Widget
樹呼叫 updateChild
方法來更新子元素。
而上一篇的 ModelBinding
呼叫 setState
的時候,因為它自身是一個 StatefulWidget
,毫無疑問它也會呼叫到 updateChild
來更新子元素。從執行結果來看,由於 ModelBinding
的例子中沒有出現重新構建 Widget
樹的情況,因此應該是在 updateChild
前的處理不同。 在 updateChild
之前會呼叫元件的 build
方法來獲取新的 Widget
樹。是這裡不同嗎?繼續往下看。
與 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
是如何感知的?比如插入了一個新的元素,或者某個元素的渲染引數變了(顏色,字型,內容等),渲染層是怎麼知道的?繼續繼續!
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)
}
從類結構上看也不復雜,這是因為大部分渲染的管理已經在父類的 ComponentElement
和 Element
中完成了。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
就和元件對應的渲染元素建立了聯絡。
接下來就是看 setState
後,怎麼獲取新的元件樹和更新元件了。我們已經知道了setState
的時候會呼叫 performRebuild
方法,在 performRebuild
中會呼叫 Element
的 updateChild
方法,現在來看InheritedElement
的updateChild
做了什麼事情。實際上 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 方法,我們在這個方法列印出來看一下。
在元素的 didChangeDependencies
中就會呼叫 markNeedsBuild
將元素標記為需要更新,然後後續的過程就和 StatefulElement
的一樣了。而對於沒有依賴狀態的元素,因為沒有在_dependent
中,因此不會被更新。
而 ModelBinding
所在的元件是 StatelessWidget
,因此最初的這個 Widget
配置樹一旦建立就不會改變,而子元件樹如果要 改變的話只有兩種情況:
1、子元件是 StatefulWidget
,通過setState
改變,那這不屬於 InheritedWidget 的範疇了,而是通過 StatefulWidget 的更新方式完成——當然,這種做法不推薦。
2、子元件的元件樹改變依賴於狀態嗎,那這個時候自然會在狀態改變的時候更新。
由此,我們終於弄明白了InheritedWidget的元件樹的感知和通知子元件重新整理過程。
總結
從 InheritedWidget 實現元件渲染的過程來看,整個過程分為下面幾個步驟:
- mount 階段將元件樹執行時型別與對應的 InheritedElement繫結,存入到 _inheritedWidgets 這個 HashMap 中;
- 在子元件新增對狀態的依賴的時候,實際上將子元件對應的 Element 元素與InheritedElement(具體的 Element 物件從_inheritedWidgets中獲取)進行了繫結,存入到了_dependents 這個 HashMap 中;
- 當狀態更新的時候,InheritedElement 直接使用舊的元件配置通知子元素的依賴發生了改變,這是通過呼叫Element 的 didChangeDependencies 方法完成的。
- 在Element的didChangeDependencies將元素標記為需要更新,等待下一幀重新整理。
- 而對於沒有依賴狀態的子元件,則不會被加入到_dependent 中,因此不會被通知重新整理,進而提高效能。
狀態管理的原理性文章講了好幾篇了,通過這些文章希望能夠達到知其然,知其所以然的目的。實際上,Flutter 的元件渲染的核心就在於如何選擇狀態管理來實現元件的渲染,這個對效能影響很大。接下來我們將以狀態管理外掛的應用方式,講述在實際例子中的應用。
我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章。
??:覺得有收穫請點個贊鼓勵一下!
?:收藏文章,方便回看哦!
?:評論交流,互相進步!