Flutter 三棵树
Flutter 三棵树
Widget :配置与描述
- 它是一个**不可变(immutable)**的蓝图。你不能在运行时去修改一个
Widget
的属性,你只能用一个新的Widget
来替换它。 - 它告诉Flutter:“我想要一个长什么样的UI元素,它应该有什么样的配置(颜色、文本、子组件等)。”
- 轻量级:创建和销毁
Widget
对象的成本极低。
Element:对比、生命周期管理与上下文
- 对比 (Diffing):这是它的核心职责之一。当新的
Widget
蓝图下来时,Element
负责将新旧Widget
进行对比,决定是更新现有节点还是销-毁并重建。这是key
发挥作用的地方。 - 生命周期管理 (Lifecycle Management):
Element
才是真正“活着”的对象。它管理着状态(对于StatefulWidget
)和生命周期(mount
,unmount
,deactivate
)。我们熟悉的State
对象就是由Element
持有和管理的。 - 上下文 (Context):每个
Element
都是一个BuildContext
。当我们在build
方法中使用context
时(例如Theme.of(context)
),我们实际上是在向Element
树查询信息。它扮演着一个“大管家”和“信息中心”的角色,连接着树中的各个节点。
RenderObject:布局计算、绘制、合成、命中测试
- 计算 (Layout):它负责所有尺寸和位置的计算。它会执行一个“2-pass layout”过程:
- 父节点向子节点传递约束(“你最多可以这么宽/高”)。
- 子节点根据约束计算自己的尺寸,并报告给父节点。
- 父节点根据子节点的尺寸,确定它们最终的位置。
- 渲染 (Painting & Compositing):
- Painting: 计算完成后,
RenderObject
负责将自己“画”出来。 - Compositing: 对于复杂的UI,它还会创建“图层”(Layers),将绘制好的内容组合起来,以便GPU可以高效地处理和渲染,实现流畅的动画和效果。
- Painting: 计算完成后,
- 命中测试 (Hit Testing):当你点击屏幕时,是
RenderObject
树负责判断你到底点中了哪个UI元素。
例子
好的,我们继续用盖房子的比喻来讲述当一个布局改变时,这三棵树是如何协同工作的。
假设我们原来的布局是这样的(这是我们的旧蓝图 Widget
树):```dart// 旧蓝图Column( children: [ Text(‘你好’), Icon(Icons.star), ],)
现在,您修改了代码,想要改变布局,把`Column`(垂直排列)换成`Row`(水平排列)。这是我们的**新蓝图 `Widget` 树**:
```dart
// 新蓝图
Row(children: [Text('你好'),Icon(Icons.star),],
)
当您保存代码并触发热重载(Hot Reload),或者调用setState
时,Flutter的更新流程就开始了。
更新流程详解
第1步:Flutter拿到新蓝图,开始与“施工队”核对
Flutter框架拿到了你的新Widget
树(Row
…),然后它会找到对应的“施工队”——Element
树。
它从Element
树的根节点开始,一层一层地向下对比新旧Widget
。
第2步:Element
树的对比与决策(最关键的一步)
Element
施工队非常聪明,它遵循两个核心原则:
- 如果新旧
Widget
的类型和key
相同,就复用这个Element
,只更新Widget
的配置。 - 如果新旧
Widget
的类型或key
不同,就丢弃旧的Element
(以及它下面的所有子Element
),创建一个全新的Element
。
现在,让我们看看施工队对比的过程:
-
对比第一层:
- 旧蓝图:
Column
- 新蓝图:
Row
- 施工队(Element)决策:“类型不同!” (
Column
!=Row
)。 “好了,旧的Column
施工员和它手下所有的人(包括Text
和Icon
的施工员)全部解雇,一个不留!” - 动作:
Column
对应的Element
被标记为“待销毁”(deactivate)。Column
对应的RenderObject
(负责垂直布局的工人)也被通知“你可以下班了”。- 所有子
Element
(Text
和Icon
的)和它们对应的RenderObject
也一并被销毁。 - 根据新的
Row
蓝图,创建一个全新的Row
的Element
。
- 旧蓝图:
-
对比第二层(在新的
Row
Element
下进行):- 新的
Row
施工员(Element
)开始为它的孩子们创建骨架。 - 它看到新蓝图里有
Text('你好')
和Icon(Icons.star)
。 - 于是,它创建了全新的
Text
的Element
和全新的Icon
的Element
。
- 新的
第3步:Element
树指挥RenderObject
树进行施工
现在,新的Element
骨架已经搭建好了,它们需要去指挥“实体工人”(RenderObject
树)干活。
- 新的
Row
的Element
会创建一个新的RenderFlex
对象(这是Row
和Column
背后的布局工人),并告诉它:“你的任务是水平排列(direction: Axis.horizontal
)。” - 新的
Text
的Element
会创建一个新的RenderParagraph
对象,并告诉它:“去把‘你好’画出来。” - 新的
Icon
的Element
会创建一个新的RenderParagraph
对象(图标在底层也是通过字体文件绘制的),并告诉它:“去把‘星星’这个图标画出来。”
第44步:RenderObject
树执行布局和绘制
现在,全新的RenderObject
工人们开始工作:
- 布局 (Layout):
Row
的RenderObject
会问它的孩子们(Text
和Icon
的RenderObject
):“你们各自想占多大地方?”Text
和Icon
的RenderObject
计算并报告自己的尺寸。Row
的RenderObject
拿到孩子们的尺寸后,按照水平方向将它们依次摆放好,计算出它们在屏幕上的最终坐标。
- 绘制 (Paint):
- 布局完成后,Flutter的渲染引擎会说:“好了,所有东西的位置和大小都定下来了,开画!”
- 它会遍历
RenderObject
树,依次调用每个工人的paint
方法,让它们把自己画在屏幕上。Text
画出文字,Icon
画出图标。
最终,你在屏幕上看到了从垂直布局变成水平布局的新界面。
如果只是改变属性,而不是类型呢?
如果我们只是把Text('你好')
改成Text('再见')
,Column
不变。
- Element对比:
- 第一层:旧
Column
vs 新Column
。类型相同!复用Column
的Element
。 - 第二层:旧
Text
vs 新Text
。类型相同!复用Text
的Element
。
- 第一层:旧
- Element决策:
Column
的Element
被复用,它对应的RenderObject
也被复用。Text
的Element
被复用,它对应的RenderObject
也被复用。
- 更新操作:
- 被复用的
Text
的Element
发现它的配置变了(文字内容从“你好”变成了“再见”)。 - 它会告诉它管理的
RenderObject
:“嘿,别的都不用变,把墙上的字擦了,重新画上‘再见’就行了。”
- 被复用的
- RenderObject工作:
Text
的RenderObject
只需要重新计算一下新文字的大小(可能需要重新布局),然后重新绘制这部分文字。Column
的RenderObject
因为孩子的大小可能变了,所以也需要重新布局,但它本身不需要重绘。
总结:
- 改变布局类型 (如
Column
->Row
):会导致Element
树和RenderObject
树在改变点大规模地销毁和重建,开销较大。 - 只改变属性 (如
color
,text
):会尽可能地复用Element
和RenderObject
,只在必要时更新属性、重新布局或重绘,开销小得多。
这就是为什么Flutter鼓励我们使用小的、组合的Widget
,并将状态管理放在尽可能低的层级,因为这样可以把UI更新的范围限制在最小,从而获得最佳性能。
Key的作用
帮助Flutter在Widget
树更新时,更精确地识别和匹配Element