Widget、Element、BuildContext 和 RenderObject
Widget
Widget
关键类及其子类继承关系如图所示:
其中,Widget
是Widget Tree
所有节点的基类。Widget
的子类主要分为3类:
-
第1类是
RenderObjectWidget
的子类,具体来说又分为SingleChildRenderObjectWidget
(单子节点容器)、LeafRenderObjectWidget
(叶子节点)、MultiChildRenderObjectWidget
(多子节点容器),它们的共同特点是都对应了一个RenderObject
的子类,可以进行Layout
、Paint
等逻辑。 -
第2类是
StatelessWidget
和StatefulWidget
,它们是开发者最常用的Widget
,自身不具备绘制能力(即不对应Render Object
),但是可以组织和配置RenderObjectWidget
类型的Widget
。 -
第3类是
ProxyWidget
,具体来说又分为ParentDataWidget
和InheritedWidget
,它们的特点是为其子节点提供额外的数据。
Element
Element
的关键类及其子类继承关系如图所示:
从图5-2中可以清楚的看到Element
的继承关系,它实现了BuildContext
接口,图5-2与图5-1相对应,每一个Element
都有一个对应的Widget
。Element
有两个直接的子类 ComponentElement
和 RenderObjectElement
,其中 ComponentElement
的两个子类 StatelessElement
和 StatefulElement
就分别对应了 StatelessWidget
和 StatefulWidget
。
我们知道最终的UI树其实是由一个个独立的Element
节点构成。组件最终的Layout、渲染都是通过RenderObject
来完成的,从创建到渲染的大体流程是:根据Widget
生成Element
,然后创建相应的RenderObject
并关联到Element.renderObject
属性上,最后再通过RenderObject
来完成布局排列和绘制。
Element
就是Widget
在UI树具体位置的一个实例化对象,大多数Element
只有唯一的renderObject
,但还有一些Element
会有多个子节点,如继承自RenderObjectElement
的一些类,比如MultiChildRenderObjectElement
。最终所有Element
的RenderObject
构成一棵树,我们称之为”Render Tree“即”渲染树“。
总结一下,我们可以认为Flutter的UI系统包含三棵树:Widget树、Element树、渲染树。他们的依赖关系是:Element树根据Widget树生成,而渲染树又依赖于Element树,如图所示。
现在我们重点看一下Element
,Element
的生命周期如下:
-
Framework 调用
Widget.createElement
创建一个Element
实例,记为element
-
Framework 调用
element.mount(parentElement,newSlot)
,mount
方法中首先调用element
所对应Widget
的createRenderObject
方法创建与element
相关联的RenderObject
对象,然后调用element.attachRenderObject
方法将element.renderObject
添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element
树结构发生变化时才需要重新添加)。插入到渲染树后的element
就处于“active
”状态,处于“active
”状态后就可以显示在屏幕上了(可以隐藏)。 -
当有父
Widget
的配置数据改变时,同时其State.build
返回的Widget
结构与之前不同,此时就需要重新构建对应的Element
树。为了进行Element
复用,在Element
重新构建前会先尝试是否可以复用旧树上相同位置的element
,element
节点在更新前都会调用其对应Widget
的canUpdate
方法,如果返回true
,则复用旧Element
,旧的Element
会使用新Widget
配置数据更新,反之则会创建一个新的Element
。Widget.canUpdate
主要是判断newWidget
与oldWidget
的runtimeType
和key
是否同时相等,如果同时相等就返回true
,否则就会返回false
。根据这个原理,当我们需要强制更新一个Widget
时,可以通过指定不同的Key
来避免复用。 -
当有祖先
Element
决定要移除element
时(如Widget
树结构发生了变化,导致element
对应的Widget
被移除),这时该祖先Element
就会调用deactivateChild
方法来移除它,移除后element.renderObject
也会被从渲染树中移除,然后Framework会调用element.deactivate
方法,这时element
状态变为“inactive
”状态。 -
“
inactive
”态的element
将不会再显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定element
,“inactive
”态的element
在当前动画最后一帧结束前都会保留,如果在动画执行结束后它还未能重新变成“active
”状态,Framework就会调用其unmount
方法将其彻底移除,这时element
的状态为defunct
,它将永远不会再被插入到树中。 -
如果
element
要重新插入到Element
树的其他位置,如element
或element
的祖先拥有一个GlobalKey
(用于全局复用元素),那么Framework会先将element
从现有位置移除,然后再调用其activate
方法,并将其renderObject
重新attach
到渲染树。
总结:
- 一个Element对象将在被创建时初始化
initial
状态,并在通过mount
方法加入Element Tree后变为active
状态;当该节点对应的Widget失效后,其自身会通过deactivate
方法进入inactive
状态。如果在当前帧的Build过程中,有其他Element
节点通过key
复用了该节点,则会通过activate
方法使得该节点再次进入active
状态;如果当前帧结束后该节点仍不在Element Tree中,则会通过unmount
方法进行卸载,并进入defunct
状态,等待后续逻辑的销毁。
看完Element
的生命周期,可能有些人会有疑问,开发者会直接操作Element树吗?
其实对于开发者来说,大多数情况下只需要关注Widget
树就行,Flutter框架已经将对Widget树的操作映射到了Element
树上,这可以极大的降低复杂度,提高开发效率。
但是了解Element
对理解整个Flutter UI框架是至关重要的,Flutter正是通过Element
这个纽带将Widget
和RenderObject
关联起来,了解Element层不仅会帮助开发者对Flutter UI框架有个清晰的认识,而且也会提高自己的抽象能力和设计能力。另外在有些时候,我们必须得直接使用Element对象来完成一些操作,比如获取主题Theme数据。
BuildContext
我们已经知道,StatelessWidget
和StatefulWidget
的build
方法都会传一个BuildContext
对象:
Widget build(BuildContext context) {
}
我们也知道,在很多时候我们都需要使用这个context
做一些事,比如:
Theme.of(context) // 获取主题
Navigator.push(context, route) // 入栈新路由
Localizations.of(context, type) // 获取Local
context.size // 获取上下文大小
context.findRenderObject() // 查找当前或最近的一个祖先RenderObject
那么BuildContext
到底是什么呢,查看其定义,发现其是一个抽象接口类:
abstract class BuildContext {
...
}
那这个context
对象对应的实现类到底是谁呢?我们顺藤摸瓜,发现build
调用是发生在StatelessWidget
和StatefulWidget
对应的StatelessElement
和StatefulElement
的build
方法中,例如在StatelessElement
中:
class StatelessElement extends ComponentElement {
...
Widget build() => widget.build(this);
...
}
同样在StatefulElement
中:
class StatefulElement extends ComponentElement {
...
Widget build() => state.build(this);
...
}
发现build
传递的参数是this
,很明显!这个BuildContext
就是StatelessElement
或StatefulElement
本身。但StatelessElement
和StatefulElement
本身并没有实现BuildContext
接口,继续跟踪代码,发现它们间接继承自Element
类,然后查看Element
类定义,发现Element
类果然实现了BuildContext
接口:
abstract class ComponentElement extends Element {
...}
abstract class Element extends DiagnosticableTree implements BuildContext {
...}
至此真相大白,BuildContext
就是widget
对应的Element
,所以我们可以通过context
在StatelessWidget
和StatefulWidget
的build
方法中直接访问Element
对象。我们获取主题数据的代码Theme.of(context)
内部正是调用了Element
的dependOnInheritedWidgetOfExactType()
方法。
总结:BuildContext
就是 Element
本尊,通过 BuildContext
的方法调用就是在操作 Element
,Widget
是外衣,而 Element
就是外衣下的裸体。
BuildContext 的另一层含义
关于 BuildContext
的另一层含义就是,它是对Widget
在Widget
树中的位置的引用,它包含了关于Widget
在Widget
树中的位置的信息,而不是关于Widget
本身的信息。
以主题为例,由于每个Widget
都有自己的BuildContext
,这意味着如果你将多个主题分散在树中,那么获取一个Widget
的主题可能会返回与另一个Widget
不同的结果。在计数器应用示例程序中的主题特定情况下,或在其他of
方法中,你将会获取到树中距离最近的该类型的父节点。
进阶
我们可以看到Element
是Flutter UI框架内部连接widget
和RenderObject
的纽带,大多数时候开发者只需要关注widget
层即可,但是widget
层有时候并不能完全屏蔽Element
细节,所以Framework在StatelessWidget
和StatefulWidget
中通过build
方法参数又将Element
对象也传递给了开发者,这样一来,开发者便可以在需要时直接操作Element
对象。
那么现在有两个问题:
1. 如果没有 widget 层,单靠 Element 层是否可以搭建起一个可用的UI框架?如果可以应该是什么样子?
2. Flutter UI 框架能不做成响应式吗?
对于问题 1,答案当然是肯定的,因为我们之前说过widget
树只是Element
树的映射,它只提供描述UI树的配置信息,Widget
就是外衣,一个人不穿衣服当然也可以比较羞耻地活着,但是穿上衣服他会活的更体面,即便不依赖Widget
我们也可以完全通过Element
来搭建一个UI框架。
下面举一个例子:
我们通过纯粹的Element
来模拟一个StatefulWidget
的功能,假设有一个页面,该页面有一个按钮,按钮的文本是一个9位数,点击一次按钮,则对9个数随机排一次序,代码如下:
class HomeView extends ComponentElement{
HomeView(Widget widget) : super(widget);
String text = "123456789";
Widget build() {
Color primary = Theme.of(this).primaryColor; //1
return GestureDetector