這兩天學習了一下 Flutter 中的 InheritedWidget 的使用方法,順便檢視一下相關原始碼瞭解了其底層實現機制。特地記錄一下。
Prerequirements
由於本文主要是從原始碼的角度分析 InheritedWidget 的工作原理,所以對閱讀本文的小夥伴的 Flutter 知識有一定的要求。主要有以下幾點,如果其中某部分你還不太清楚,請先閱讀相關連結:
- 瞭解 Flutter 的基本用法。
- 瞭解 Flutter 中的 Widget 和 Element 的基本概念。 推薦閱讀:Flutter, what are Widgets, RenderObjects and Elements?
- 對 Flutter 中 Element 的生命週期有基本瞭解。推薦閱讀:Element class
下面開始正文。
InheritedWidget 的使用方法
先看一個 InheritedWidget 最簡單的使用示例:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyWelcomeInfo extends InheritedWidget {
MyWelcomeInfo({Key key, this.welcomeInfo, Widget child})
: super(key: key, child: child);
final String welcomeInfo;
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return oldWidget.welcomeInfo != welcomeInfo;
}
}
class MyNestedChild extends StatelessWidget {
@override
build(BuildContext context) {
final MyWelcomeInfo widget =
context.inheritFromWidgetOfExactType(MyWelcomeInfo);
return Text(widget.welcomeInfo);
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter InheritWidget',
home: MyWelcomeInfo(
welcomeInfo: 'hello flutter',
child: Center(
child: MyNestedChild(),
)),
);
}
}
複製程式碼
可以看出我們使用 InheritedWidget 時涉及到的工作量主要有 2 部分:
- 建立一個繼承自 InheritedWidget 的類,並將其插入 Widget 樹
- 通過 BuildContext 物件提供的
inheritFromWidgetOfExactType
方法查詢 Widget 樹中最近的一個特定型別的 InheritedWidget 類的例項
這裡還暗含了一個邏輯,那就是當我們通過 inheritFromWidgetOfExactType
查詢特定型別 InheritedWidget 的時候,這個 InheritedWidget 的資訊是由父元素層層向子元素傳遞下來的呢?還是 inheritFromWidgetOfExactType
方法自己層層向上查詢的呢?
接下來讓我們從原始碼的角度分別看看 Flutter 框架對以上幾部分的實現。
實現原理分析
InheritedWidget 定義
首先看一下 InheritedWidget 的定義:
abstract class InheritedWidget extends ProxyWidget {
const InheritedWidget({ Key key, Widget child })
: super(key: key, child: child);
@override
InheritedElement createElement() => new InheritedElement(this);
@protected
bool updateShouldNotify(covariant InheritedWidget oldWidget);
}
複製程式碼
它是一個繼承自 ProxyWidget
的抽象類。內部沒什麼邏輯,除了實現了一個 createElement
方法之外,還定義了一個 updateShouldNotify()
介面。 每次當 InheritedElement 的例項更新時會執行該方法並傳入更新之前對應的 Widget 物件,如果該方法返回 true
那麼依賴該 Widget 的(在 build 階段通過 inheritFromWidgetOfExactType
方法查詢過該 Widget 的子 widget)例項會被通知進行更新;如果返回 false
則不會通知依賴項更新。這個機制和 React 框架中的 shouldComponentUpdate
機制很像。
InheritedWidget 相關資訊的傳遞機制
每個 Element 例項上都有一個 _inheritedWidgets
屬性。該屬性的型別為:
HashMap<Type, InheritedElement>
複製程式碼
其中儲存了祖先節點中出現的 InheritedWidget 與其對應 element 的對映關係。在 element 的 mount
階段和 active
階段,會執行 _updateInheritance()
方法更新這個對映關係。
對於普通 Element 例項,_updateInheritance()
只是單純把父 element 的 _inheritedWidgets
屬性儲存在自身 _inheritedWidgets
裡。從而實現對映關係的層層向下傳遞。
void _updateInheritance() {
assert(_active);
_inheritedWidgets = _parent?._inheritedWidgets;
}
複製程式碼
由 InheritedWidget 建立的 InheritedElement 重寫了該方法:
void _updateInheritance() {
assert(_active);
final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null)
_inheritedWidgets = new HashMap<Type, InheritedElement>.from(incomingWidgets);
else
_inheritedWidgets = new HashMap<Type, InheritedElement>();
_inheritedWidgets[widget.runtimeType] = this;
}
複製程式碼
可以看出 InheritedElement 例項會把自身的資訊新增到 _inheritedWidgets
屬性中,這樣其子孫 element 就可以通過前面提到的 _inheritedWidgets
的傳遞機制獲取到此 InheritedElement 的引用。
InheritedWidget 的更新通知機制
首先讓我們回答一個小問題,前文提到 _inheritedWidgets
屬性存在於 Element 例項上,而我們程式碼中呼叫的 inheritFromWidgetOfExactType
方法則存在於 BuildContext 例項之上。那麼 BuildContext 是如何獲取 Element 例項上的資訊的呢?答案是不需要獲取。因為每一個 Element 例項也都是一個 BuildContext 例項。這一點可以從 Element 的定義中得到:
abstract class Element extends DiagnosticableTree implements BuildContext {
}
複製程式碼
而每次 Element 例項執行 Widget 例項的 build
方法時傳入的 context 就是該 Element 例項自身,以 StatelessElement 為例:
class StatelessElement extends ComponentElement {
...
@override
Widget build() => widget.build(this);
...
}
複製程式碼
大家也可以在 IDE 中通過斷點驗證這一點。
既然可以拿到 InheritedWidget 的資訊了,那接下讓我們通過原始碼看看更新通知機制的具體實現。
首先看一下 inheritFromWidgetOfExactType
的實現:
InheritedWidget inheritFromWidgetOfExactType(Type targetType) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
assert(ancestor is InheritedElement);
_dependencies ??= new HashSet<InheritedElement>();
_dependencies.add(ancestor);
ancestor._dependents.add(this);
return ancestor.widget;
}
_hadUnsatisfiedDependencies = true;
return null;
}
複製程式碼
首先在 _inheritedWidget
對映中查詢是否有特定型別 InheritedWidget 的例項。如果有則將該例項新增到自身的依賴列表中,同時將自身新增到對應的依賴項列表中。這樣該 InheritedWidget 在更新後就可以通過其 _dependents
屬性知道需要通知哪些依賴了它的 widget。
每當 InheritedElement 例項更新時,會執行例項上的 notifyClients
方法通知依賴了它的子 element 同步更新。notifyClients
實現如下:
void notifyClients(InheritedWidget oldWidget) {
if (!widget.updateShouldNotify(oldWidget))
return;
assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
for (Element dependent in _dependents) {
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));
dependent.didChangeDependencies();
}
}
複製程式碼
首先執行相應 InheritedWidget 上的 updateShouldNotify
方法判斷是否需要通知,如果該方法返回 true
則遍歷 _dependents
列表中的 element 並執行他們的 didChangeDependencies()
方法。這樣 InheritedWidget 中的更新就通知到依賴它的子 widget 中了。
寫在最後
本文簡單的從原始碼的角度討論了 InheritedWidget 的實現原理。由於 Flutter 框架本身也是使用 Dart 語言開發的,在熟悉了 Dart 語言後其實閱讀 Flutter 原始碼並沒有想象中的那麼難,大家也可以根據自己感興趣的點去有選擇的閱讀部分原始碼。
由於作者也是 Flutter 的初學者,文中可能存在描述不準確甚至是錯誤的地方。歡迎大家一起討論。