flutter: 建樹流程

環境: flutter sdk v1.7.8+hotfix.4@stableandroid

對於界面開發,一般的視圖樹都是經過視圖對象持有父節點與子節點列表而創建的雙向節點樹,如android中的View(子節點抽象)與ViewParent(父節點抽象,ViewGroup是其實現體),ViewGroup顯式的持有了View類型的對象數組,經過各類dispatchXXX的方法將事件/操做/更新傳遞給子節點;但在flutter中彷佛有些不一樣,Widget(視圖控件抽象)沒有區分父子關係,關鍵是Widget抽象里根本沒有表明子節點與父節點的成員!它最關鍵的方法僅爲createElement,那個extends DiagnosticableTree看上去很可疑,但只要細察下代碼,基本上是爲了方便調試而打印信息的。因此雖然能夠在各類文章和代碼中看到樹的指稱,但須要首先搞清這個樹究竟指的是什麼樹。git

樹的含義

固然是Element樹!雖然對於熟悉以往界面開發的人來講這個結論有點讓人狐疑,但咱們應該明確的獲得確定:就是這樣,由於從任意一個控件抽象Widget出發,沒法到達Widget根節點或者任何Widget子節點,也就是沒法實施遍歷操做,固然也就不是樹形數據結構了。對於Web開發的人來講比較容易接受,常常在涉及Web的開發談到Element,android的開發如今須要習慣這種指稱,默認的樹指的就是Element樹,不然理解就容易產生歧義,同時以前文章所說的Widget樹這種說法是錯誤的,由於根本就沒有Widget樹!github

如前文所述像RenderObjectToWidgetAdapter這樣的Widget不就顯式的持有了一個Widget做爲child成員嗎?的確,但這樣的持有是具體類子類的持有,仍是沒法經過訪問成員再訪問到它的子節點,這個聯繫根本就是中斷的。數組

因此建樹就是創建Element樹,訪問Widget也只能經過Element間接訪問:在Element定義中能夠看到它直接持有了一個Widget,訪問到了Element也就訪問到了Widget,這是從android轉過來的開發人員須要反覆銘記的一點。Element有一個_parent做爲其成員,所以能夠上溯到根節點的Widget,然而使人困惑的是Element並無Element數組或者列表來表明子節點!那Element是如何訪問子節點的?bash

遍歷子節點

基類Element並無直接持有數組或者列表來訪問子節點,而是經過visitChildren的空實現體方法,方法參數(ElementVisitor)自己是一個方法(typedef ElementVisitor = void Function(Element element); framework.dart:1794)。數據結構

這不就是個訪問者模式嗎,然而爲何要這麼搞?這麼作的意圖是但願徹底由Element子類型來決定訪問Element子節點的順序,爲遍歷操做提供更大的靈活性,子節點的持有仍是須要的,只不過由Element子類型具體實現。這是能夠想到的,顯然,若是咱們在基類型持有了子節點,那遍歷子節點就有了默認順序。譬如android中的ViewGroup, 從頭至尾的子視圖列表順序表明了由下到上的層次關係(ZOrder),但不得再也不提供相似getChildDrawingOrder方法來讓子類型有改變訪問順序的機會。less

遍歷形式從直接持有變成方法傳遞,這樣作也是有缺點和風險的,那就是可能在運行期動態的改變訪問子節點的順序而形成視圖數據的紊亂!因此在這個方法上也有明確的註釋說明訪問順序保持一致的重要性:ui

/// There is no guaranteed order in which the children will be visited, though /// it should be consistent over time.this

在創建樹的過程當中也不能調用這方法,由於訪問的多是舊的子節點或者子節點尚未徹底創建。這樣看來直接持有Element子節點未必就很差。spa

創建樹的過程

Element對象是如何一步步構建成樹形結構的?雖然在Element代碼定義上有一些註釋能夠參考建樹的關鍵步驟,但最好仍是從入口調用分析來看:

WidgetsBinding.attachRootWidget
  RenderObjectToWidgetAdapter.attachToRenderTree
    BuildOwner.buildScope
      RenderObjectToWidgetElement.mount
        RootRenderObjectElement.mount(null, null)
          RenderObjectElement.mount
            Element.mount
            RenderObjectWidget.createRenderObject => RenderObjectToWidgetAdapter.createRenderObject
        RenderObjectToWidgetElement._rebuild
          Element.updateChild
            Element.inflateWidget
              Widget.createElement => MyApp
              Element.mount
複製代碼

這裏涉及了一大坨Element類型及其方法,有些是自有方法,有些是覆蓋方法,有些是基類方法,這個時候只能一步步分析,避免混亂。

RenderObjectToWidgetElementRenderObjectToWidgetAdapter這個Widget具體建立的Element類型,顯式的調用了mount方法,而且傳入的參數均爲(null, null),前面的文章已說明RenderObjectToWidgetElement是真正的Element根節點。關鍵是它是如何串連起其它Element對象的?

由以上調用序列可知RenderObjectToWidgetElement.mount最終調用了Element.moutElement.mout其實就是創建指向關係,但它是根節點,不用再指向父節點,只須要關注其子節點建立,再看是如何關聯子節點的。RenderObjectToWidgetElement有一個顯式的成員_child, 是一個Element類型,發現其是在RenderObjectToWidgetElement._rebuild中被賦值的,而_rebuild又是在RenderObjectToWidgetElement.mount的實現體中被調用,這樣走到了一個關鍵方法Element.updateChild,從其註釋就能夠看出來:

This method is the core of the widgets system.

經過兩個重要參數爲null與否,Element.updateChild區分了4種具備不一樣含義的操做,當前只需關注child == null && newWidget != null這種狀況,從其註釋看這正是建立子節點的途徑!細分的調用序列以下:

Element.updateChild
  Element.inflateWidget
    Widget.createElement => MyApp
    Element.mount
複製代碼

針對child == null && newWidget != null這種狀況Element.updateChild最終調用的是Element.inflateWidget,注意這個名稱有誤導性,從代碼可知當前Element沒有對Widget有任何操做,只是調用了Widget.createElement, 而這個Widget對象是從外部傳入的,不是當前Element本身持有的!具體的,這個Widget對象應該是當前Element關聯的Widget對象的子對象(widget.childwidgets/binding.dart:939),對應的正是咱們自定義的MyApp!

因此新建立的子Element是由子Widget建立,接着又調用了子Element的mount方法,傳入的parent參數是this(newChild.mount(this, newSlot); framework.dart:3084),即將當前Element做爲父節點與新建節點Element關聯,這個mount很是形象的表現了一個新建節點掛在一個即有節點之上的操做,因而子節點的mount繼續以上過程直至創建最終的節點。

如此看來,flutter的Element更像是一個衣物掛鉤,它創建的樹形結構更像前向單鏈表網,而鉤子正是Element._parent

再看Widget關聯

最開始說Widget並不持有子Widget,那麼Element在mount的時候當前Widget又是如何提供子Widget來建立子Element的呢?

答案是仍是要看當前Element具體操做mount的方式。譬如咱們的根ElementRenderObjectToWidgetElement直接用了自身持有的根WidgetRenderObjectToWidgetAdapter持有的child來關聯了咱們傳入的MyApp做爲子Widget。

再譬如一個比較重要的Element類型ComponentElement:它是在mount的時候調用了一個自身的抽象方法Widget build() (framework.dart:3950), 這裏返回的Widget對象正是當前Element須要建立的子Widget。而ComponentElement有兩個最重要的實現類覆蓋了Widget build() 方法:StatelessElement是經過持有的StatelessWidget對象再去建立一個子Widget對象;StatefulElement是經過持有的StatefulWidget對象建立的State<StatefulWidget>(framework.dart:3989)再去建立子Widget的。咱們的MyApp再去建立它的子Widget時就是經過此類方式,由於MyApp是一個StatelessWidget對象,MyApp建立的Element是StatelessElement類型。

再譬如RenderObjectElementmount時還建立了RenderObject,而且關聯父RenderObject,而這個父RenderObject未必是父Element關聯的RenderObject(_findAncestorRenderObjectElementframework.dart:4950);

因此大部分Widget的父子關係並非持有關係而是建立關係,而且是在Element.mount的時機建立的,建立後也並不持有!

結論

創建Element樹最重要的操做就是Element.mount

每一種具體類型的Element,實現瞭如何將當前Element掛接(mount)到父節點上的操做;這個掛接操做除了與父Element創建指向關係外,還規定了當前Element的一些其它屬性的建立時機和操做。

建立一個Element最重要的操做就是Element.updateChild

更具體的是Element.inflateWidget方法;經過建立子Widget方式的不一樣,區分了兩大類Element和Widget: (StatelessElement, StatelessWidget)和(StatefulElement, StatefulWidget)

所謂的Element樹更像是前向單鏈表網,單鏈表有共同的表頭。

父類Element不持有Element子節點,而是經過Element.visitChildren把遍歷操做交給具體的Element子類型來實現。

可是RenderObject卻像普通的單鏈表,由於經過mixin RenderObjectWithChildMixin<RenderObject>提供的child, RenderObject可以直接遍歷子節點。

相關文章
相關標籤/搜索