Widget
原文地址: www.flutterinternals.org/data-model/…
什么是 Widget?
- Widget 提供了一个对用户界面不可变的描述。尽管 widgets 本身是不可变的,但它们可以被自由的替换、删除或重新排列(请注意,更新 widgets 的子节点通常也需要替换父 widget)。创建和销毁窗口 widget 是非常高效的,因为窗口 widget 是轻量级的,不可变的实例,理想情况下它们是编译时的常量。
- 不可变的 widget tree 用于创建和配置(即 inflate)可变 element tree,element tree 又管理着 render tree;render tree 负责布局,绘制,手势和合成。element tree与 wideget 的更改高效同步,并在可能的情况下重用和改变 element(也就是说,尽管小部件可以用不同的实例替换,但只要两个实例具有相同的运行时类型和 key,那么原先的 element 将被更新而不是重新创建)。修改 element tree 通常会更新 render tree,而 render tree 又会改变屏幕上显示的内容。
- 主要的 widget 类型是
RenderObjectWidget
,StatefulWidget
和 StatelessWidget
。那些将数据导出到一个或多个后代 widget(通过通知或另一种机制)的 widget 可以利用ProxyWidget
或其子类之一(例如,InheritedWidget
或 ParentDataWidget
)。
- 通常,widget 通过修改 element tree 直接或间接配置 render object。开发者创建的大多数 widget(通过
StatefulWidget
和 StatelessWidget
)通常通过 build 方法(例如StatelessWidget.build
)委托给一组后代 widget。其他 widget(例如RenderObjectWidget
)直接管理 render object(分别通过RenderObjectWidget.createRenderObject
和 RenderObjectWidget.updateRenderObject
创建和更新 render object)。
- 某些 widget 通过
ProxyWidget
封装显式的子 widget,引入可继承状态(例如,InheritedWidget
,InheritedModel
)或配置辅助数据(例如,ParentDataWidge
t)。
ProxyWidget
通知 clients(通过 ProxyElement.notifyClients
)以响应 widget 的变更(通过 ProxyElement.updated
,此方法由 ProxyElement.update
调用)。
ParentDataWidget
更新最近的后代 render object 的 parent data(通过ParentDataElement._applyParentData
,这个方法还调用了 RenderObjectElement._updateParentData
);每当相应的 widget 更新时,都会触发此过程。
- 还有一些定制的 widget 子类,它们支持一些不太常见的配置。例如,
PreferredSizeWidget
继承了 Widget 并获取一个优先尺寸,从而允许子类(例如AppBar
,TabBar
,PreferredSize
)向其容器(例如 Scaffold
)传递尺寸信息。
LeafRenderObjectWidget
,SingleChildRenderObjectWidget
和 MultiChildRenderObjectWidget
为具有零个或多个子节点的 render object widget 提供存储,而不会限制底层 render object 的创建或更新方式。这些 widget 分别对应于LeafRenderObjectElement
,SingleChildRenderObjectElement
和 MultiChildRenderObjectElement
,它们管理底层的位于 element tree 和 render tree 中子模型。
- 使用
Builder
和 StatefulBuilder
创建匿名 widget。
Stateless Widget 是如何工作的?
StatelessWidget
是 Widget
的一个普通子类,它定义了 StatelessWidget.build
方法并配置了 StatelessElement
。
StatelessElement
是 ComponentElement
子类,该子类调用 StatelessWidget.build
来响应 StatelessElement.build
(例如,将构建过程委托给它的 widget )。
Stateful Widget 是如何工作的?
StatefulWidget
与 StatefulElement
相关联,StatefulElement
与 StatelessElement
几乎相同。关键区别在于,StatefulElement
保留了对相对应的 StatefulWidget
的 State
的引用,并在该实例上调用了方法,而不是 widget 本身。例如,当调用 StatefulElement.update
时,通过 State.didUpdateWidget
通知 State
实例。
StatefulElement
在构造时创建关联的 State
实例(即StatefulWidget.createElement
)。然后,当第一次创建 StatefulElement
时(通过 StatefulElement.mount
调用的 StatefulElement._firstBuild
),会调用 State.initState
。至关重要的是,State
实例和 StatefulWidget
引用相同的 element。
- 由于
State
与底层的StatefulElement
相关联,因此,如果 widget 发生变化,只要 StatefulElement.updateChild
能够重用同一 element(因为 widget 的 runtime type 和 key 都相同),则将保留 State
。否则,将重新创建 State
。
为什么改变树的深度是昂贵的?
- Flutter 没有比较树的能力。也就是说,在匹配 widget 和 element 时,只考虑元素的直系子代(通过
RenderObjectElement.updateChildren
)。
- 当增加树的深度时(即插入一个中间节点),现有的父节点将配置一个与中间节点对应的子节点。在大多数情况下,这个 widget 不会对应于以前的子节点(即
Widget.canUpdate
将返回 false)。因此,新的 element 将被重新 inflated。由于这个中间节点是其父节点的子节点的新父节点,所以这些子节点中的每个子节点也将被 inflate(中间节点没有访问现有 element 的权限)。这将沿着整个子树向下进行。
- 当减少树的深度时,父节点将再次被分配新的子节点,这些子节点很可能不会与旧的子节点同步。因此,新的子 element 将需要被 inflated,在整个子树上层层递进。
- 给上一个子节点添加一个
GlobalKey
可以缓解这个问题,因为Element.updateChild
能够重用存储在GlobalKey
注册表中的 element(允许该子树简单地重新插入而不是重建)。
Notifications 是如何工作的?
- 对 Notification 的支持不是直接建立在 widget 抽象中的,而是分层在其之上。
Notification
是一个抽象类,它在 element tree 中进行搜索,访问 NotificationListener
的每个子类 widget (Notification.dispatch
调用 Notification.visitAncestor
并执行此遍历)。
- Notification 在每个适当的 widget 上调用
NotificationListener._dispatch
,将通知的静态类型与回调的类型参数进行比较。如果有匹配(即 notification 是回调的类型参数的子类型),则调用 listener。
- 如果 listener 返回 true,则遍历终止。否则,notification 会继续在树上向上冒泡。