Flutter 必知必会系列 —— Element 更新实战

2,916 阅读9分钟

这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战

往期精彩

👉 Flutter 必知必会系列——三颗树到底是什么

👉 Flutter 必知必会系列—— Element 的更新复用机制

前面我们已经了解了三棵树、Element 的更新服用,都有点偏理论,这一篇文章,就从几个实际的例子来检验之前的知识点。

以下是正文


案例简介

案例的基本功能就是点击按钮交换色块
案例的详细代码可以在这里看 👉 案例代码

核心代码如下:

StatelessColor

使用 StatelessWidget 显示色块,代码如下:

class StatelessColor extends StatelessWidget {
  final Color defaultColor = UniqueColorGenerator().getColor();

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 100,
      width: 100,
      child: Container(
        color: defaultColor,
      ),
    );
  }

  StatelessColor({Key? key}) : super(key: key);
}

StatefulColor

使用 StatefulWidget 显示色块,代码如下:

class StatefulColorfulTileState extends State<StatefulColor> {
  final Color defaultColor = UniqueColorGenerator().getColor();

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 100,
      width: 100,
      child: Container(
        color: defaultColor,
      ),
    );
  }
}

UniqueColorGenerator

随机产生颜色,代码如下:

class UniqueColorGenerator {
  List<Color> colorList = [
    Colors.blue,
    Colors.yellow,
    Colors.red,
    Colors.black54,
    Colors.greenAccent,
    Colors.pinkAccent
  ];

  Random random = Random();

  Color getColor() {
    return colorList[random.nextInt(6)];
  }
}

案例一:交换 Stateless的Color

我们看运行的代码:

class _SwapColorDemo1State extends State<SwapColorDemo1> {
  late List<Widget> widgets;

  @override
  void initState() {
    super.initState();
    widgets = [StatelessColor(), StatelessColor()]; //第一处
  }

  swapTile() {
    setState(() {
      widgets.insert(1, widgets.removeAt(0)); //第三处
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Stateless'),
      ),
      body: SafeArea(
        child: Row( // 第二处
          children: widgets,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.swap_horiz),
        onPressed: swapTile, //第三处
      ),
    );
  }
}

我们看这就是一个很简单的页面,页面上有两个随机颜色的100*100的色块
点击右下角的按钮,交换色块,并刷新页面。\

第一处代码:在页面初始化的时候,初始化两个色块。
第二处代码:使用 Row 组件,包裹两个色块。
第三处代码:点击按钮的时候,刷新页面,实现交换色块的功能

widgets.insert(1, widgets.removeAt(0)); 
 
数组一开始 :A  B

widgets.removeAt(0) 取得是A,并且数组是 B

insert 在1号位插入A
 
结果就是:B A

后续同样的道理,就实现了交换。

大家觉得可以实现交换吗?肯定是可以的 😄。

image.png

下面我们来看原因。通过前面我们的几篇文章知道了:Widget 是要显示的 UI 配置,Element 根据 Widget 去真正的显示 UI。 由于我们的颜色存在 Widget 中,那能做到图块交换的,就是以下几种情况:

Element 的 widget 指向了 新Widget,比如原来 Element 指向的 粉红色的 Widget,现在指向的是黄色 Widget。

根据交换后的 Widget,重新生成 Element。比如把原来承载红色 Widget 的 Element,销毁掉并根据黄色 Widget 的生成 Element。

直接交换 Element 。比如把红色和黄色的 Element 位置进行交换

下面我们就看一下,我们的代码属于上面的哪一种情况。

当我们点击按钮的时候,会触发 build 方法,进而触发 Row 控件的更新,Row 对应的 Element 是 MultiChildRenderObjectElement,它的更新就会执行到 👉 updateChildren 方法

Row 的子组件 是这样的:

案例一初始化代码.png

因此在 updateChildren 方法中,我们只会停留在第一步的循环 ----- 自上而下 diff 并更新子节点

因为 Widget.canUpdate 返回的是true。 新旧的 Widget 的 runtimeType 都是 StatelessColor,并且由于我们没有手动设置 Key,所以新旧 Widget 的 key 属性是 null。

所以 canUpdate 返回 true。 framework 就认为要复用 Element 了,所以执行到了更新 child 的操作。

👇我们看是如何更新 child 的。

这里回忆一下我们之前讲过的,updateChild 的表格:

表格.png

案例一中的入参情况:

child 是旧 Element newWidgets 是要显示的 widget,这里就是我们自定义的Widget,交换之后的 Widget

通过上面的更新判断表格,执行复用更新的逻辑。

 Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
        ...
        //key和runtimeType一样 就可以复用 第四处
        child.update(newWidget);
        ...
  }

执行的是 第四处代码的逻辑。 child 是承载 StatelessColor 的 Element,而 StatelessColor 就是 StatelessWidget,所以 child 就是 StatelessElement

还记得 StatelessElement 的更新逻辑吗? 就是调用其 Widget 的 build 方法。

Untitled Diagram.drawio.png

显示的 UI 就是 Element 持有的 Widget 对象的 build 方法返回的 UI,我们将颜色的信息保存在 Widget 中,因此就会正常的互换颜色。

案例一.gif

图中的底块代表的是 Element,文字代表 Widget。Element没有改变,只是将指向的 Widge t改变了,这就是颜色互换现象的原因。

小结

StatelessWidget 颜色替换的原因:

  • 复用了 Element,并更新了 Element 指向的 Widget
  • Widget 是 StatelessWidget,颜色存储在Widget 中。

案例二:交换 Stateful 的Color

页面的主体代码和功能不变 👉 详细代码,只是将 StatelessColor 替换为 StatefulColor

案例二 33%.gif

从现象来看,图块没有进行交换

那我们看一下,为啥从 StatelessWidget 替换为 StatefulWidget 之后,颜色咋就不变化了。

和案例一的分析 相似,同样会执行到 updateChildren 方法。

image-20220129141216945.png

并且在 updateChildren 方法中,我们同样只会停留在第一步的循环 ----- 自上而下 diff 并更新子节点

有了案例一分析,我们直接走到 updateChild 中,只不过每个 child 的类型从 StatelessElement 变为 StatefulElement。调用流程如下:

Untitled Diagram.drawio (1).png

和案例一的 StatelessElemen t不同,这里 Element 生成的 UI 是 State对象的 build 方法,我们的颜色也是存储在 State 中的,而 State 的初始化时机是在 Element 初始化的时候。如下代码:

StatefulElement(StatefulWidget widget)
    : _state = widget.createState(),
      super(widget) {
  state._element = this;
  state._widget = widget;
}

也就是说:Element 和 State 对象是同生共死的,Element 是复用的,因此 Element 的 State 引用是不变的,而我们颜色存储在 State 对象中,从而导致:Element 只是替换了 Widget,颜色没有变化。

更新过程如下:

案例二 原因图.gif

小结

StatefulWidget 颜色不替换的原因:

  • 复用了 Element,并更新了 Element 指向的 Widget
  • Element 持有的 State 引用没有变化,颜色存储在 State 对象中

案例三:交换带有 Key 属性的 Stateful 的Color

在详细介绍之前,我们介绍一个点 Slot 槽点信息。

Slot 槽点信息

槽点 是指 Element 在其父节点的位置,如果父 Element 是单节点的 Element,那么子节点的槽点就是 null。

如果父 Element 是多节点的 Element,那么子节点的槽点就是位置信息(IndexedSlot)
IndexedSlot 是一个封装类,封装了 index 索引和槽点值,第一个子节点的槽点值是 null,第二个子节点的槽点值是前一个节点,以此次类推。、 因此通过槽点,就可以得到位置。

槽点的确定是在 mount 方法中

单节点的槽点:

单节点槽点.png

多节点的槽点: 多节点槽点.png

updateChild 的处理过程中,我们有一判断点就是比较槽点信息。

///省略代码
 Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
    final Element newChild;
    if (child != null) {
      bool hasSameSuperclass = true;
      if (hasSameSuperclass && child.widget == newWidget) { //第一处
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      } 
    return newChild;
  }

如果新旧 Widget 是同一个 Widget。在新一帧的显示中,位置发生了变化,只更新一下位置信息。

案例分析

页面的主体代码和功能不变 👉 详细代码,和案例二相比 StatefulColor 增加了 Key 属性。

案例三原图33%.gif

从现象来看,图块进行了交换

还记得我们之前说的可能的原因吗?

一:Element 的 widget 指向了 新Widget,比如原来 Element 指向的 粉红色的 Widget,现在指向的是黄色 Widget。

二:根据交换后的 Widget,重新生成 Element。比如把原来承载红色 Widget 的 Element,销毁掉并根据黄色 Widget 的生成 Element。

三:直接交换 Element 。比如把红色和黄色的 Element 位置进行交换

案例一能够交换是因为第一点原因。

下面我们就看一下,案例三属于上面的哪一种情况。

和上面的分析相似,同样会执行到 updateChildren 方法。

案例三代码.png

和案例一、二不一样,updateChildren 方法代码会走到 ----- 存储可复用的 Element

因为每次diff的时候,key是不同的,所以跳过了步骤一 和 步骤二 比如原来第一个位置的 Element 的 Widget 的 key 是 key1,要显示的 Widget 的 key 是 key2,所以 diff 都会跳过

这里走完第三步骤之后Map的情况如下:

key1:Element1(Widget是1)
key2:Element2(Widget是2)

按照处理的流程:Widget2 的 key 是 key2,发现 Map 中有 key2,那就取出来 Element2。 但是原来 Element2 的前一个节点是Element1,现在Element2要显示在第一个位置,所以它的 slot 变为了null

处理完第三步,就开始逐步的更新。现在原来显示 Widget1 的地方,现在要显示 Widget2 。

所以:

 final Element newChild = updateChild(oldChild, newWidget,previousChild);
 入参情况:
    oldChild:是Element2,并且Element2的widget是Widget2
    newWidget:是widget2
    previousChild: null
-----------------------------------------------------------------------------
    oldChild:是Element1,并且Element1的widget是Widget1
    newWidget:是widget1
    previousChild: Element2

根据入参数的情况,updateChild 方法也是执行到了 updateChild 的代码第三处,但是装在新旧 Widget 相同,但是槽点信息不一样,从而将 Element 的位置进行了交换,尽而 State 进行了交换,所以颜色才会交换。

更新过程如下:

案例三原因图.gif

注意这种情况下,State 的 build 方法也不会执行哦~。

小结

设置 Key 之后,StatefulWidget 颜色进行了替换的原因:

  • 复用了 Element,Element 的 Widget、State 信息都进行了保留
  • 更新了 Element 的位置信息

案例四

我们给 StatefulWidget 包裹一个 Padding,代码案例在SwapColorDemo4类中。但我们点击按钮的时候,色块竟然随机出现了

案例四原图33%.gif

从现象来看,图块进行了随机交换。和案例三相比的话,只是包裹了一层。经过了前面的分析,我们先考虑一个问题,什么情况下会随机出现颜色?

我们知道颜色存在 State 中,那就是承载 State 的 Element 重新创建了。

那什么时候会重新创建呢,无法复用的时候。

那么接下来,我们就看 StatefulWidget 的 Element 的存活情况。

在处理 Padding 的这一层的时候,处理的流程和案例一相似。只会停留在第一步的循环 ----- 自上而下 diff 并更新子节点

因为新旧的 Widget 的 runtimeType 都是Padding,并且由于我们没有手动设置 Key,所以新旧 Widget的key 属性是 null。所以 canUpdate 返回 true。
framework 认为要复用了,所以执行到了更新 child 的操作。

  final Element newChild = updateChild(oldChild, newWidget, previousChild);
  入参情况:
   oldChild是旧Padding的Element
   newWidget是新Padding

执行 Padding 的 update 方法,接下来的就不用介绍了吧。
在显示 Padding 的 child 的时候,发现两个 child 的 Key 不一样,也就是执行到了 updateChild 的重新构造子节点的逻辑,所以保存颜色的 State,也进行了重新构造。

更新过程如下:

案例四原因图.gif

小结

包裹 Padding 之后,StatefulWidget 颜色随机出现的原因:

  • 复用了 Padding 的 Element,但是由于 Padding 的 child 带有Key,因此 Padding 的子节点不会复用

总结

这个几个小例子,我们就知道 Flutter 99% 的更新逻辑了,还剩 1% 是 InheritedWidget,这个我们后面接着奏乐接着舞~。