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可以直接遍历子节点。

相关文章
相关标签/搜索