Flutter小知識--what is key?

在Flutter中,每一個Widget都是惟一標記的。這個惟一標識是有框架在編譯/渲染期間定義的。
Widget的惟一標識與其可選參數Key一致。若是不傳Flutter會爲你生成一個。
在某些狀況下,你可能須要強制指定它的key,這樣你就能根據key來訪問一個widget。
爲了實現這樣一個需求,你可使用下面輔助工具中的一個:GlobalKey,LocalKey,UniqueKey或者ObjectKey。
其中GlobalKey能夠保證在整個應用程序中惟一。git

GlobalKey相關概念

整個應用程序惟一的key。github

Global keys能夠惟一標識elements。Global keys提供了訪問與elements關聯的其餘對象的能力,好比 StatefulWidgets的BuildContextStateBuildContextcanvas

擁有global keys的Widgets當他們從樹的一個位置挪到另外一個位置,能夠爲子樹重定Widget。爲了可以重定父級,一個widget必須在同一個tree中,同一個動畫幀中,離開原來原位置,併到達新的位置。緩存

Global keys是至關昂貴的。若是你不須要上面列舉的功能,能夠考慮 Key,ValueKey,ObjectKey或者UniqueKey來代替。bash

你不能在同一個樹下包含兩個擁有相同global key的widget。若是嘗試這麼作將會促發運行時斷言。框架

GlobalKey的定義以下:less

abstract class GlobalKey<T extends State<StatefulWidget>> extends Key
複製代碼

T必需要繼承自State,能夠說這個GlobalKey專門用於組件了.ide

static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
複製代碼

GlobalKey裏含有一個Map,key和value分別爲自身和Element。
那何時會用到這個Map尼?
跟蹤代碼很快就找到Element類的mount方法:函數

void mount(Element parent, dynamic newSlot) {
    ...
    if (widget.key is GlobalKey) {
      final GlobalKey key = widget.key;
      key._register(this);
    }
   ...
  }
複製代碼

可見GlobalKey會在組件Mount階段把自身放到一個Map裏面緩存起來。
緩存又有何做用尼?
答案依然是爲了性能。
思考一個場景,A頁面是一個商品列表有許多商品圖片(大概就單列這樣),B頁面是一個商品詳情頁(有商品大圖),當用戶在A頁面點擊一個其中詳情,可能會出現一個過渡動畫,A頁面的商品圖片慢慢放大而後下面的介紹文字也會跟着出現,而後就這樣平滑的過渡到B頁面。
此時A頁面和B頁面都其實共用了一個商品圖片的組件,B頁面不必重複建立這個組件能夠直接把A頁面的組件「借」過來。工具

總之框架要求同一個父節點下子節點的Key都是惟一的就能夠了,GlobalKey能夠保證全局是惟一的,因此GlobalKey的組件可以依附在不一樣的節點上。
而從GlobalKey對象上,你能夠獲得幾個有用的屬性currentElement,currentWidget,currentState。

接下來看一下Widget自己的key定義.

Widget.key

用來控制在樹中,一個widget如何替換另外一個widget。

若是兩個widgets的runtimeTypekey屬性x相等(operator==),那麼新的widget替換就得widget經過更新底層的element(調用新的widget的Element.update)。 除此以外,舊的element將會從tree中移除,新的widget將會轉化成element,並被插入到tree中。

另外,使用GlobalKey做爲widget的key將容許element在tree中移動(經過變化parent),並不會丟失state.當一個新的widget被發現(它的key和type跟上一個在同一個位置的widget都不一樣), 可是有一個擁有相同global key的widget在上一幀的tree的其餘地方,那麼這個widget的element將被移動到新的位置。

一般來講,一個widget若是是另一個widget的惟一child,此時沒必要擁有一個明確的key.

這裏屢次提到element,那麼element究竟是什麼?

Element

提到Element,須要再提一下Widget的官方定義。

/// Describes the configuration for an [Element].
///
/// Widgets are the central class hierarchy in the Flutter framework. A widget
/// is an immutable description of part of a user interface. Widgets can be
/// inflated into elements, which manage the underlying render tree.
複製代碼

能夠看到,Widget 的實際工做也就是描述如何建立 ElementWidget 是一個不可變對象,它能夠被複用, 請注意,這裏的複用不是指在兩次渲染的時候將對象從舊樹中拿過來放到新樹,而是在同一個 Widget Tree 中,某個子 Widget 能夠出現屢次,由於它只是一個 description。
Widget 只是 Element 的一個配置描述 ,告訴 Element 這個實例如何去渲染。

/// A given widget can be included in the tree zero or more times. In particular
/// a given widget can be placed in the tree multiple times. Each time a widget
/// is placed in the tree, it is inflated into an [Element], which means a
/// widget that is incorporated into the tree multiple times will be inflated
/// multiple times.
複製代碼

從上面這段註釋能夠看出,Widget 和 Element 之間是一對多的關係。實際上渲染樹是由 Element 實例的節點構成的樹,而做爲配置文件的 Widget 可能被複用到樹的多個部分,對應產生多個 Element 對象。

這也就是你每次均可以在 build() 函數中新建 widget 的緣由。構建 widget 的過程並不耗費資源,由於 Wiget 只是用來保存屬性的容器。

若是widget只是提供給用戶的包裝殼,那麼實際進行渲染的是什麼呢?
答案是RenderingObject。有一個很好的源碼案例可參見Opacity的源碼,地址以下.
Opacity源碼

選用這個例子的緣由是 普通的Stateless / StatefulWidget 只是將其餘 Widget 組裝起來,而 Opacity 會真正地影響 Widget 的繪製。
最終會跟到RenderOpacity中,會看到下面的方法:

@override
void paint(PaintingContext context, Offset offset) {
    context.pushOpacity(offset, _alpha, super.paint);
}
複製代碼

完整代碼
PaintingContext 就是進行繪製操做的畫布,這裏經過在 canvas 上調用名爲pushOpacity的方法來實現不透明度的控制。

總結一下,Widget 只是一個配置,RenderObject 負責管理佈局、繪製等操做。而鏈接WidgetRenderObject正是Element

而在 Element的源碼中,則能夠獲取到RenderObject:

/// The render object at (or below) this location in the tree.
  ///
  /// If this object is a [RenderObjectElement], the render object is the one at
  /// this location in the tree. Otherwise, this getter will walk down the tree
  /// until it finds a [RenderObjectElement].
  RenderObject get renderObject {
    RenderObject result;
    void visit(Element element) {
      assert(result == null); // this verifies that there's only one child if (element is RenderObjectElement) result = element.renderObject; else element.visitChildren(visit); } visit(this); return result; } 複製代碼

能夠大體總結出三者的關係是:配置文件 Widget 生成了 Element,然後建立 RenderObject 關聯到 Element 的內部 renderObject 對象上,最後Flutter 經過 RenderObject 數據來佈局和繪製。

參考資料以下,感謝:
www.stephenw.cc/2018/05/28/…
juejin.im/post/5b4c60…


若是你以爲這篇文章對你有益,還請幫忙轉發和點贊,萬分感謝。

Flutter爛筆頭
相關文章
相關標籤/搜索