共计 4891 个字符,预计需要花费 13 分钟才能阅读完成。
环境: flutter sdk v1.7.8+hotfix.4@stable
对于界面开发,通常的视图树都是通过视图对象持有父节点与子节点列表而建立的双向节点树,如 android 中的 View(子节点抽象)与 ViewParent(父节点抽象,ViewGroup 是其实现体),ViewGroup 显式的持有了 View 类型的对象数组,通过各种 dispatchXXX 的方法将事件 / 操作 / 更新传递给子节点;但在 flutter 中似乎有些不同,Widget(视图控件抽象)没有区分父子关系,关键是 Widget 抽象里根本没有代表子节点与父节点的成员!它最关键的方法仅为 createElement
,那个extends DiagnosticableTree
看上去很可疑,但只要细察下代码,基本上是为了方便调试而打印信息的。所以虽然可以在各种文章和代码中看到树的指称,但需要首先搞清这个树究竟指的是什么树。
树的含义
当然是 Element 树!虽然对于熟悉以往界面开发的人来说这个结论有点让人狐疑,但我们应该明确的得到肯定:就是这样,因为从任意一个控件抽象Widget
出发,无法到达 Widget
根节点或者任何 Widget
子节点,也就是无法实施遍历操作,当然也就不是树形数据结构了。对于 Web 开发的人来说比较容易接受,经常在涉及 Web 的开发谈到 Element,android 的开发现在需要习惯这种指称,默认的树指的就是 Element 树,否则理解就容易产生歧义,同时之前文章所说的 Widget 树这种说法是错误的,因为根本就没有 Widget 树!
如前文所述像 RenderObjectToWidgetAdapter
这样的 Widget
不就显式的持有了一个 Widget
作为 child
成员吗?的确,但这样的持有是具体类子类的持有,还是无法通过访问成员再访问到它的子节点,这个联系根本就是中断的。
所以建树就是建立 Element 树,访问 Widget
也只能通过 Element
间接访问:在 Element
定义中可以看到它直接持有了一个 Widget
,访问到了Element
也就访问到了 Widget
,这是从 android 转过来的开发人员需要反复铭记的一点。Element
有一个 _parent
作为其成员,因此可以上溯到根节点的 Widget
,然而令人困惑的是Element
并没有 Element
数组或者列表来代表子节点!那 Element
是如何访问子节点的?
遍历子节点
基类 Element
并没有直接持有数组或者列表来访问子节点,而是通过 visitChildren
的空实现体方法,方法参数 (ElementVisitor
) 本身是一个方法(typedef ElementVisitor = void Function(Element element);
framework.dart:1794)。
这不就是个访问者模式吗,然而为什么要这么搞?这么做的意图是希望完全由 Element 子类型来决定访问 Element 子节点的顺序,为遍历操作提供更大的灵活性,子节点的持有还是需要的,只不过由 Element 子类型具体实现。这是可以想到的,显然,如果我们在基类型持有了子节点,那遍历子节点就有了默认顺序。譬如 android 中的 ViewGroup
, 从头到尾的子视图列表顺序代表了由下到上的层次关系(ZOrder),但不得不再提供类似getChildDrawingOrder
方法来让子类型有改变访问顺序的机会。
遍历形式从直接持有变成方法传递,这样做也是有缺点和风险的,那就是可能在运行期动态的改变访问子节点的顺序而造成视图数据的紊乱!所以在这个方法上也有明确的注释说明访问顺序保持一致的重要性:
/// There is no guaranteed order in which the children will be visited, though
/// it should be consistent over time.
在建立树的过程中也不能调用这方法,因为访问的可能是旧的子节点或者子节点还没有完全建立。这样看来直接持有 Element
子节点未必就不好。
建立树的过程
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 并不持有子 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 树最重要的操作就是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
能够直接遍历子节点。