Flutter三棵树
你写的是。
我们可以把这三棵树看作是一个建筑工程的不同阶段:
1. Widget 树:配置清单(蓝图)
Widget 是你最熟悉的。它是声明式的,描述了 UI 应该长什么样。
-
特点:不可变(Immutable)、轻量级。
-
角色:它只是一份“配置说明书”。当你调用
setState时,旧的 Widget 树会被丢弃,重新生成一份新的。 -
比喻:就像你点餐时填写的菜单勾选单。写坏了?撕了重写一张就行,成本极低。
2. Element 树:管理调度(项目经理)
Element 是连接 Widget 和 RenderObject 的桥梁。它是持久化的。
-
特点:可变(Mutable)、维护状态(State)。
-
角色:它负责决定是否需要更新底层的渲染。当 Widget 树变动时,Element 会进行 Diff 算法对比:如果 Widget 的类型(Type)和 Key 没变,它就只更新配置,而不会销毁重建。
-
比喻:就像餐厅服务员。他拿着你的新菜单,对比旧菜单,发现你只是把“加辣”改成了“微辣”,于是告诉后厨:“菜不用重炒,多放点水涮涮就行。”
3. RenderObject 树:实际渲染(施工队)
RenderObject 负责最终的布局(Layout)和绘制(Painting)。
-
特点:非常重、计算量大。
-
角色:它是真正的“干活人”。它知道每一个像素点该画在哪,处理具体的测绘逻辑(Size、Position)。
-
比喻:就像厨师。不论菜单怎么换,厨师(RenderObject)是相对固定的,他负责开火、颠勺,把菜做出来。
三棵树的协作流程
为了让你更直观地对比,我们可以看下这个表格:
| 特性 | Widget | Element | RenderObject |
| 生命周期 | 极短(瞬时创建/销毁) | 较长(通常与页面同寿) | 较长 |
| 主要职责 | 描述 UI 结构 | 生命周期管理、Diff 对比 | 布局、绘制 |
| 性能开销 | 极小(简单对象) | 中等 | 很大(计算复杂) |
为什么需要三棵树?
如果只有一棵树(比如直接操作 RenderObject),每次修改 UI 都要重新计算所有布局和绘制,性能会瞬间崩溃。
-
解耦:Widget 层让开发者只需要关注“逻辑和样式”,不用管底层怎么画。
-
性能优化:Element 层的 Diff 算法通过复用 RenderObject,极大地减少了昂贵的渲染开销。
一句话总结:
你写的是 Widget(我想吃什么),Flutter 帮你维护 Element(谁在吃,状态对不对),最后指派 RenderObject(把菜炒出来)。
追问1:Diff算法是什么?
在 Flutter 的世界里,Diff 算法(差异算法)就是 Element 树用来实现“偷懒”的核心逻辑。
当 Widget 树发生变化时,Flutter 不会傻乎乎地把屏幕上的东西全部删掉重画。相反,它会对比新旧 Widget 树,找出那些没变的部分并复用它们,只更新真正变了的地方。
1. 核心公式:canUpdate
Diff 算法的灵魂缩写成一行代码,就是 Widget.canUpdate。当 Flutter 遍历新 Widget 树时,它会拿着新 Widget 去问旧 Element:“你对应的旧 Widget 能不能被我这个新 Widget 替换?”
Dart
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
-
类型(runtimeType)一样:比如都是
Text。 -
Key 一样:如果没有设置 Key,默认为
null,也算一样。
如果这两者都相同,Element 就会说:“没问题,我还能用,只要更新一下属性就行!”
2. Diff 算法的三个原则
Flutter 的 Diff 算法为了效率,做了一些非常聪明的“假设”,使其复杂度从 $O(n^3)$ 降低到了 $O(n)$。
原则 A:同层对比(Level-by-level)
Flutter 只会对比同一层级的节点。如果一个 Padding 变成了 SizedBox,Flutter 不会去对比它们的子节点,而是直接把这棵子树拆了重建。
原则 B:类型不同,直接重建
如果 Widget 的类型变了(比如从 Container 变成 Text),那么以这个节点为根的整棵子树都会被销毁并重新创建。
原则 C:Key 是唯一凭证
在处理同级兄弟节点(比如 ListView 里的列表项)的移动、插入或删除时,Key 是 Diff 算法识别“谁是谁”的唯一证据。
3. 两种处理结果:复用 vs 销毁
| 场景 | canUpdate 结果 | 发生的事情 | 性能开销 |
| 属性改变 (例如颜色变了) | true |
复用 Element,更新 RenderObject 属性 | 极小 (仅更新) |
| 类型改变 (例如 Text 换成了 Image) | false |
销毁旧 Element,创建新 Element 和渲染对象 | 较大 (重新布局绘制) |
| 位置移动 (有 Key 时) | true |
移动 Element 到新位置,复用状态 | 小 (仅移动指针) |
4. 为什么要理解它?
如果你不理解 Diff 算法,可能会遇到以下尴尬:
-
性能杀手:在列表里频繁切换不同类型的 Widget 导致频繁销毁重构。
-
状态丢失:当你交换两个有状态(StatefulWidget)的子组件位置时,如果没有加
Key,你会发现它们的状态(比如输入框里的文字)并没有跟着换,甚至会乱套。
一句话总结:
Diff 算法就是拿着新蓝图对比老房子,能刷墙漆(更新属性)解决的,绝对不拆迁(重建 Element)。
追问2:Element 树是如何通过 Key 来优化渲染性能的
我们通常说 Key 是为了性能,但更准确地说,Key 是为了在“动荡”的 Widget 树中,帮 Element 找回它失散的“灵魂伴侣”(State)。
1. 为什么需要 Key?(没有 Key 的惨案)
假设你有一个列表,里面有两个色块组件(StatefulWidget),分别是 A(红色)和 B(蓝色)。
如果你把它们的位置对调了,发生了什么?
-
Widget 层:[A, B] 变成了 [B, A]。
-
Element 层:
-
对比第一个位置:旧的是 A 类型,新的是 B 类型。
-
关键点:如果不加 Key,
canUpdate只看类型(runtimeType)。既然 A 和 B 都是同一个类,canUpdate返回true! -
结果:Element 觉得自己没变,只是属性变了。它会保留原来的 Element(以及它持有的 State),只把 Widget 里的颜色换掉。
-
-
后果:如果 A 里面有一个输入框或者倒计时(存在 State 里),你会发现颜色换了,但输入的内容或者倒计时还在原地。这就产生了 Bug。
2. Key 是如何解决问题的?
当你加上 Key 后,canUpdate 的逻辑变成了:Type 一样 且 Key 一样。
-
对比第一个位置:旧 A 的 Key 是 "1",新 B 的 Key 是 "2"。
-
判断:
canUpdate返回false。 -
Element 的骚操作:Flutter 不会直接销毁 Element,而是会去旧的 Element 列表中查找,看看有没有 Key 为 "2" 的 Element。
-
匹配成功:它发现第二个位置的 Element 正好 Key 是 "2",于是直接把整个 Element 节点(连同它内部的 State)挪到第一个位置来。
结论:Key 保证了 Widget -> Element -> State 这条线的“强绑定”关系,无论你怎么移动 Widget,对应的状态都会跟着走。
3. 常见的 LocalKey 类型
在同一层级的兄弟节点中,我们需要使用 LocalKey。主要有以下三种:
| Key 类型 | 使用场景 | 示例 |
ValueKey |
以一个简单值(字符串、数字)作为标识。 | ValueKey(item.id) |
ObjectKey |
以一个对象实例作为标识。 | ObjectKey(user) |
UniqueKey |
每次构建都生成一个独一无二的 Key。 | UniqueKey() |
⚠️ 注意:千万不要在
build方法里直接写key: UniqueKey()。因为每次
build都会生成新 Key,导致canUpdate永远返回false,所有的 Element 和 State 都会在每次刷新时被销毁重建,性能会掉进地狱。
4. 什么时候必须用 Key?
其实并不是所有地方都要用 Key。只有同时满足以下 3 个条件 时,Key 才是必须的:
-
它是
StatefulWidget。 -
它在 列表或集合 中(有多个兄弟节点)。
-
这个列表会发生 顺序变化、插入或删除。
如果你的组件是 StatelessWidget,不用 Key 也通常没问题(除非涉及复杂的 Hero 动画),因为它们没有 State 需要保留,重建的开销极小。
5. 性能优化的真相
与其说 Key 优化了性能,不如说 Key 决定了“复用”的粒度。
-
不加 Key:Flutter 倾向于“原地复用”,只更新属性。
-
加了 Key:Flutter 能够实现“跨位置复用”,让整个重量级的 State 对象跟着 Widget 跑,避免了重新初始化 State 的昂贵开销。
一句话总结:
Key 是 Element 的身份证。有了它,Element 才能在 Widget 乱跑的时候,精准地带着它的 State 嫁到正确的位置。
追问3:const在diff算法中起到什么作用
在 Flutter 的渲染机制和 Diff 算法中,const 关键字扮演着**“性能加速器”的角色。它的核心作用是实现编译时常量化和对象复用**,从而在 Widget 树比对时跳过不必要的更新。
以下是 const 在 Diff 过程中起到的具体作用:
1. 内存中的“唯一实例”
当你使用 const 构造一个 Widget 时,Dart 编译器会保证在整个应用生命周期内,无论你在多少个地方写了这个 const 调用,它们在内存中都指向同一个地址(同一个实例)。
-
非 const: 每次
build都会在堆上创建一个新的对象。 -
const: 整个应用只创建一次,后续所有
build只是在引用同一个内存指针。
2. 绕过 Diff 的“捷径”
Flutter 的 Element 树在决定是否需要重新构建(rebuild)子节点时,会进行一个非常高效的判断。
在 performRebuild 过程中,Flutter 会检查新旧 Widget 是否完全相同(Identity equality):
Dart
// 简化的 Flutter 内部逻辑
if (oldWidget == newWidget) {
// 如果两个 Widget 指向同一个内存地址(即它们都是同一个 const 实例)
// Flutter 会认为这一层及其子树完全没有变化
// 直接跳过 build,性能开销几乎为零!
}
3. 阻断“重绘链”
在 Flutter 中,当一个 StatefulWidget 的 setState 被触发时,默认情况下它所有的子 Widget 都会重新 build。
-
如果没有 const: 父组件
build-> 创建新的子组件 Widget 实例 -> Diff 算法发现引用变了 -> 触发子组件重新build。 -
如果有 const: 父组件
build-> 引用已有的const子组件实例 -> Diff 算法发现oldWidget === newWidget-> 停止向下递归。
这就是为什么 const 可以有效地阻断重绘的传递,将更新范围控制在最小的局部。
4. 实例对比
情况 A:普通构造
Dart
@override
Widget build(BuildContext context) {
return Column(
children: [
Text("Counter: $count"), // 每次都变
MyHeavyWidget(), // 每次父组件 build,这里都会跟着重新 build,造成浪费
],
);
}
情况 B:使用 const
Dart
@override
Widget build(BuildContext context) {
return Column(
children: [
Text("Counter: $count"),
const MyHeavyWidget(), // 即使父组件重绘,这里因为引用没变,会被 Flutter 直接跳过
],
);
}
总结
| 维度 | 非 const Widget | const Widget |
| 创建时机 | 运行时(每次 build) | 编译时(仅一次) |
| 内存占用 | 随 build 次数增加 | 始终只有一个实例 |
| Diff 结果 | old != new (即使内容相同) |
old == new (指针相同) |
| 性能损耗 | 需要递归对比子树 | 直接跳过子树对比 |
一句话总结: const 让 Flutter 的 Diff 算法从“深层比对内容”变成了“极速比对指针”,是优化 Flutter 性能最简单、成本最低的手段。
更多推荐



所有评论(0)