從 Flutter 原始碼看 InheritedWidget 內部實現原理 | 掘金技術徵文

loveky發表於2018-07-18

這兩天學習了一下 Flutter 中的 InheritedWidget 的使用方法,順便檢視一下相關原始碼瞭解了其底層實現機制。特地記錄一下。

Prerequirements

由於本文主要是從原始碼的角度分析 InheritedWidget 的工作原理,所以對閱讀本文的小夥伴的 Flutter 知識有一定的要求。主要有以下幾點,如果其中某部分你還不太清楚,請先閱讀相關連結:

下面開始正文。

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 的初學者,文中可能存在描述不準確甚至是錯誤的地方。歡迎大家一起討論。


從 0 到 1:我的 Flutter 技術實踐 | 掘金技術徵文,徵文活動正在進行中

相關文章