Widget更新机制
可以看视频:www.youtube.com/watch?v=kn0…
首先讲一下Widget、Element、RenderObject之间的关系
- Widget对象都是Immutable对象,它是Element对象的「描述文件」,通过Widget对象生成对应的Element对象,他们之间是一一对应的关系。
- Element:Element是Flutter中负责管理和更新Widget树的对象。每个Widget在其生命周期中都会创建一个与之关联的Element。Element负责根据Widget的配置创建、更新和管理RenderObject。
Element对象持有对应的Widget和RenderObject对象的引用 - RenderObject:RenderObject是Flutter中负责实际绘制UI的对象。
Widget、Element和RenderObject之间的关系可以总结如下:
- Widget是描述UI的配置对象,它们定义了UI的结构和外观。
- Element是负责管理和更新Widget树的对象,它们根据Widget的配置创建和管理RenderObject。
Element对象持有对应的Widget和RenderObject对象的引用 - RenderObject是负责实际绘制UI的对象,它们根据Element提供的布局信息在屏幕上绘制UI。
生成关系是:
Widget → Element → RenderObject
Widget是如何更新的?
主要代码:
@immutableabstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
···
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
Widget.canUpdate方法决定了是否使用新的Widget去「更新(Update)」对应Element中的旧Widget。
💡 「此处需要特别注意「更新(Update)」和重建(Rebuild)之间的区别」在Flutter中,更新(Update)和重建(Rebuild)是两个不同的概念,它们在应用程序的生命周期中起着重要作用。
- 更新(Update):
更新是指在Flutter框架检测到应用程序的状态发生变化时,对widget树进行调整的过程。在更新过程中,Flutter会比较新旧widget树,以确定如何有效地更新UI。更新过程通常涉及到以下操作:
- 重新配置现有的**
Element**,以便它们使用新的属性值。 - 保留现有的**
Element**及其状态,以便在新的widget树中重用。 - 在需要时,创建新的**
Element**。 - 移除不再需要的**
Element**。
在更新过程中,Flutter尽量避免不必要的重新创建**Element,以提高性能。通过使用key属性,可以帮助Flutter更有效地匹配新旧widget树中相应的Element**。
- 重建(Rebuild):
重建是指根据应用程序的状态重新构建(build)widget树的过程。当应用程序的状态发生变化时,Flutter会调用**build**方法来创建新的widget树。重建过程通常涉及以下操作:
- 调用**
build**方法以创建新的widget树。 - 检查widget树中的依赖关系,以确定哪些部分需要重建。
- 通知**
Element和RenderObject**更新它们的状态和UI。
重建是Flutter响应式编程模型的核心部分,它确保应用程序的UI始终与其状态保持同步。
简而言之,更新是调整现有widget树以匹配新状态的过程,而重建是根据新状态重新构建widget树的过程。在应用程序的生命周期中,这两个过程共同确保UI始终与应用程序的状态保持同步。
Widget 只是一个配置且无法修改,而 Element 才是真正被使用的对象,并可以修改。
当新的 Widget 到来时将会调用 canUpdate 方法,来确定这个 Element是否需要更新。
canUpdate 对两个(新老) Widget 的 runtimeType 和 key 进行比较,从而判断出当前的 Element 是否需要更新。若 canUpdate 方法返回 true 说明不需要替换 Element,直接更新其引用的 Widget对象即可。
举个栗子🌰:点击按钮,交换两个色块的位置
StatelessWidget实现
class StatelessContainer extends StatelessWidget {
final Color color = RandomColor().randomColor();
@override Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: color,
);
}
}
class Screen extends StatefulWidget {
@override
_ScreenState createState() => _ScreenState();
}
class _ScreenState extends State<Screen> {
List<Widget> widgets = [
StatelessContainer(),
StatelessContainer(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: widgets,
),
),
floatingActionButton: FloatingActionButton(
onPressed: switchWidget,
child: Icon(Icons.undo),
),
);
}
switchWidget(){
widgets.insert(0, widgets.removeAt(1));
setState(() {});
}
}
点击右下角按钮会交换两个色块的位置,下面将分析为什么会导致更新。
当前的各组成部分关系如上图所示,在点击切换后,Widget树中元素位置首先发生改变。
StatelessContainer 比较过程
在两个Widget交换位置之后,Element树会从上到下,按层级进比较。
首先是RowElement,然后是它的child。
接下来RedElement check左边对应层级的「新Widget」,也就是变化位置之后的BlueWidget。比较新老两个Widget的key和runtimeType是否相同。 因为StatelessWidget没有Key,所以默认他们Key相同,只比较runtimeType即可(此处RuntimeType = Container)。如果相同,canUpdate 方法返回 true,Element就会更新Widget对象引用,指向新的Widget。 StatelessElement 调用新持有 Widget 的 build 方法重新构建,而我们的 color 实际上就是储存在 widget 中的,因此在屏幕上两个 Widget 便被正确的交换了顺序。
StatefulContainer 比较过程
class StatefulContainer extends StatefulWidget {
StatefulContainer({Key key}) : super(key: key);
@override _StatefulContainerState createState() => _StatefulContainerState();
}
class _StatefulContainerState extends State<StatefulContainer> {
final Color color = RandomColor().randomColor();
@override Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: color,
);
}
}
而在 StatefulContainer 的例子中,我们将 color 的定义放在了 State 中,Widget 并不保存 State,真正 hold State 的引用的是 Stateful Element。
💡 注意:页面上最终展示什么内容,是Element和它包含的State对象共同决定的当我们没有给 Widget 任何 key 的时候,将会只比较这两个 Widget 的 runtimeType 。由于两个 Widget 的属性和方法都相同,canUpdate 方法将会返回 true,于是更新 StatefulWidget 的位置,这两个 Element 将不会交换位置。但是原有 Element 只会从它持有的 state 实例中 build 新的 widget。因为 element 没变,它持有的 state 也没变。所以颜色不会交换。这里变换 StatefulWidget 的位置是没有作用的。
class _ScreenState extends State<Screen> {
List<Widget> widgets = [
StatefulContainer(key: UniqueKey(),),
StatefulContainer(key: UniqueKey(),),
];
而我们给 Widget 一个 key 之后,canUpdate 方法将会比较两个 Widget 的 runtimeType 以及 key。并返回 false。(这里 runtimeType 相同,key 不同)。随后Element会deactivate原来引用指向的Widget对象,并且在「同一层级」中通过新Widget的key去寻找对应的新Widget,并将引用指向新的Widget。 State是随Element走的。
对于这个例子来讲就是交换了 Element 的位置并交换了对应 renderObject 的位置。都交换了,那么颜色自然也就交换了。
💡 什么是「同一层级」?比如:
List<Widget> widgets = [
StatefulContainer1(key: UniqueKey(),),
StatefulContainer2(key: UniqueKey(),),
];
StatefulContainer1 和 StatefulContainer2 就是同一层级。
但是:
List<Widget> widgets = [
Padding1(
padding: const EdgeInsets.all(8.0),
child: StatefulContainer1(key:UniqueKey()),
),
Padding2(
padding: const EdgeInsets.all(8.0),
child: StatefulContainer2(key:UniqueKey()),
),
];
此时,StatefulContainer1和StatefulContainer2之间就不在同一层级。 因为Padding1 下面只有StatefulContainer1,Padding2同理。 只有,有共同「直接Ancestor」的widget才是同一层级。
如果Element通过新Widget的key在同一层级找不到对应的Widget,那么就会deactive oldWidget并且移除这个Element,然后根据新的Widget在同样的位置重建(Rebuild)一个新的Widget.