環境: 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類型及其方法,有些是自有方法,有些是覆蓋方法,有些是基類方法,這個時候只能一步步分析,避免混亂。
RenderObjectToWidgetElement
是RenderObjectToWidgetAdapter
這個Widget
具體建立的Element
類型,顯式的調用了mount
方法,而且傳入的參數均爲(null, null),前面的文章已說明RenderObjectToWidgetElement是真正的Element根節點。關鍵是它是如何串連起其它Element對象的?
由以上調用序列可知RenderObjectToWidgetElement.mount
最終調用了Element.mout
,Element.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.child
widgets/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,那麼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
類型。
再譬如RenderObjectElement
在mount
時還建立了RenderObject
,而且關聯父RenderObject
,而這個父RenderObject
未必是父Element
關聯的RenderObject
(_findAncestorRenderObjectElement
framework.dart:4950);
因此大部分Widget的父子關係並非持有關係而是建立關係,而且是在Element.mount
的時機建立的,建立後也並不持有!
Element.mount
。每一種具體類型的Element,實現瞭如何將當前Element掛接(mount
)到父節點上的操做;這個掛接操做除了與父Element創建指向關係外,還規定了當前Element的一些其它屬性的建立時機和操做。
Element.updateChild
。更具體的是Element.inflateWidget
方法;經過建立子Widget方式的不一樣,區分了兩大類Element和Widget: (StatelessElement, StatelessWidget)和(StatefulElement, StatefulWidget)
父類Element不持有Element子節點,而是經過Element.visitChildren
把遍歷操做交給具體的Element子類型來實現。
可是RenderObject
卻像普通的單鏈表,由於經過mixin RenderObjectWithChildMixin<RenderObject>
提供的child, RenderObject
可以直接遍歷子節點。