Flutter key 色块交换 updateChild child.update理解

参考:https://www.jianshu.com/p/6e704112dc67

示例1:StatelessWidget能够交换色块
示例2:StatefulWidget不能交换色块
示例3:StatefulWidget外部传入颜色能够交换色块

首先是基于原生ios的理解当时的想法是 Widget类比UIView 靠 位置都换了颜色为啥不换 ???理解不能

现在想想Widget只是个配置描述 Element才可以类比UIView

本文以下讨论全部是基于runtimetype相同且key相同(null)

1、2的代码可参考https://www.jianshu.com/p/6e704112dc67
3的代码如下 Widget build(BuildContext context) {}进行了简化方便打断点调试
_SwapColorDemo1State

class _SwapColorDemo1State extends State<SwapColorDemo2> {
  List<Widget> widgets;
  @override
  void initState() {
  // A.runtimeType == B.runtimeType;
    super.initState();
    widgets = [
      StatefulColorfulTile(defaultColor: Colors.green),
      StatefulColorfulTile(defaultColor: Colors.red,)
    ];
  }

  @override
  Widget build(BuildContext context) {

    return GestureDetector(
      onTap: swapTile,
      child: Row(
        children: widgets,
      ),
    );
  }



  swapTile() {
    print("swap");
    setState(() {
      widgets.insert(1 , widgets.removeAt(0));
    });
  }
}

StatefulColorfulTile

class StatefulColorfulTile extends StatefulWidget {
  final Color defaultColor;
  StatefulColorfulTile({this.defaultColor,Key key}) : super(key: key);

  @override
  StatefulColorfulTileState createState() => StatefulColorfulTileState();
}

class StatefulColorfulTileState extends State<StatefulColorfulTile> {
  // final Color defaultColor = UniqueColorGenerator().getColor();
  @override
  void didUpdateWidget(covariant Widget oldWidget) {
    super.didUpdateWidget(oldWidget);
  }
  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 100,
      width: 100,
      child: Container(
        color: widget.defaultColor,
      ),
    );
  }
}

对于:https://www.jianshu.com/p/6e704112dc67在学习key的用法时对这个无法交换色块的表现感到很奇怪,但对解释有点一脸懵逼
尤其是对于StatefulWidget的解释一头雾水 猛一看其实差不多 其实还是不一样的

1:所以 Element 树将根据 Widget 树进行对应的更新。
2:Flutter 使用 Element 树和它对应的控件的 State 去确定要在设备上显示的内容, 所以 Element 树没有改变

当我们交换行中的两个色块时,Flutter 遍历 Widget 树,看看骨架结构是否相同。它从 Row Widget 开始,然后移动到它的子 Widget,Element 树检查 Widget 是否与旧 Widget 是相同类型和 Key。 如果都相同的话,它会更新对新 widget 的引用。在我们这里,Widget 没有设置 Key,所以Flutter只是检查类型。它对第二个孩子做同样的事情。所以 Element 树将根据 Widget 树进行对应的更新。

现在,我们点击按钮,交换控件的次序,Flutter 将遍历 Element 树,检查 Widget 树中 Row 控件并且更新 Element 树中的引用,然后第一个 Tile 控件检查它对应的控件是否是相同类型,它发现对方是相同的类型; 然后第二个 Tile 控件做相同的事情,最终就导致 Flutter 认为这两个控件都没有发生改变。Flutter 使用 Element 树和它对应的控件的 State 去确定要在设备上显示的内容, 所以 Element 树没有改变,显示的内容也就不会改变。

不符合直观逻辑尤其是无法解释 示例3中 添加了外部defaultColor时为何色块又能交换了,因为runtimetype和key都是相等的。

为了寻求答案查了两天 先贴下用到的知识储备
·StatelessWidget中
第一次构建StatelessElement createElement() => StatelessElement(this);
StatelessElement中
Widget build() => widget.build(this);
即StatelessElement调用build其实调用的是StatelessWidget的Widget build(BuildContext context)

对于StatefulWidget
StatefulElement createElement() => StatefulElement(this);

第一次创建StatefulElement时将_state和StatefulElement进行了一对一绑定
这个绑定是始终不变的

The State instance associated with this location in the tree.
There is a one-to-one relationship between State objects and the StatefulElement objects that hold them. The State objects are created by StatefulElement in mount.

第一次创建时
StatefulElement的widget是StatefulWidget
StatefulElement的_state是widget.createState()
_state的_element是StatefulElement
_state的_widget是StatefulElement的widget

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

//此处其实是调用的_state的build方法
Widget build() => _state.build(this);

stateful和stateless执行逻辑是相同的

首先父Widget发起

updateChild=>child.update=>rebuild=>performRebuild=>build()

方法实现中唯一的不同在child.update

updateChild

Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  if (newWidget == null) {
    if (child != null)
      deactivateChild(child);
    return null;
  }
  Element newChild;
  if (child != null) {
    bool hasSameSuperclass = true;
      final int oldElementClass = Element._debugConcreteSubtype(child);
      final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
      hasSameSuperclass = oldElementClass == newWidgetClass;
    if (hasSameSuperclass && child.widget == newWidget) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      newChild = child;
    } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      child.update(newWidget);
      newChild = child;
    } else {
      deactivateChild(child);
      newChild = inflateWidget(newWidget, newSlot);
    }
  } else {
    newChild = inflateWidget(newWidget, newSlot);
  }
  return newChild;
}

stateful的update

//stateful的update
void update(StatefulWidget newWidget) {
  super.update(newWidget);
  final StatefulWidget oldWidget = _state._widget;
  // Notice that we mark ourselves as dirty before calling didUpdateWidget to
  // let authors call setState from within didUpdateWidget without triggering
  // asserts.
  _dirty = true;
  _state._widget = widget as StatefulWidget;
  rebuild();
}

stateless的update

//stateless的update
void update(StatelessWidget newWidget) {
    super.update(newWidget);
    _dirty = true;
    rebuild();
  }

rebuild=> performRebuild

performRebuild()

盯住这儿

void performRebuild() {
   Widget built;
#盯住这儿
     built = build();

    _dirty = false;
    
    _child = updateChild(_child, built, slot);
    assert(_child != null);
    if (!kReleaseMode && debugProfileBuildsEnabled)
    Timeline.finishSync();
}

1:当交换StatelessWidget色块时 函数调用流程如下

第一步:Element updateChild(Element child, Widget newWidget, dynamic newSlot) {}
第二步:child.update(newWidget);

class StatelessElement extends ComponentElement 
//StatelessElement的update
void update(StatelessWidget newWidget) {
    super.update(newWidget);
    _dirty = true;
    rebuild();
  }
rebuild=>performRebuild()

第三步:调用了build(),build是在Widget中的Widget build(BuildContext context)函数 使用了newWidget的build()所以色块能够发生交换

void performRebuild() {
   Widget built;
     built = build();
    _dirty = false;
    
    _child = updateChild(_child, built, slot);
    assert(_child != null);
    if (!kReleaseMode && debugProfileBuildsEnabled)
    Timeline.finishSync();
}

从StatelessColorfulTile对应的Element的父Element开始调用 父Element开始更新子Element

Element updateChild(Element child, Widget newWidget, dynamic newSlot) {}
此时this是child的父Element child是StatelessColorfulTile对应的Element
因为runtimetype和key相等调用到child.update(newWidget);

在child.update(newWidget)中 newWidget是交换后的色块
child.update=>rebuild()=>performRebuild()=>build()
对于StatelessElement是newWidget的build()所以色块发生了交换

总结如下:

1:element在elementtree中没有交换位置
2:element对应的widget发生了交换
3:调用了newWidget的build()导致颜色发生了改变

2:当交换StatefulWidget时

update变成了如下方法

//stateful的update
void update(StatefulWidget newWidget) {
  super.update(newWidget);
  final StatefulWidget oldWidget = _state._widget;
  // Notice that we mark ourselves as dirty before calling didUpdateWidget to
  // let authors call setState from within didUpdateWidget without triggering
  // asserts.
  _dirty = true;
  _state._widget = widget as StatefulWidget;
  rebuild();
}

1:super.update(newWidget);将当前element的widget更新为newWidget
2:_state._widget = widget 将(当前element对应的)_state的_widget更新为
newWidget

后续和StatelessElement一样去调用

performRebuild()
built = build();

三者统一了 走的方法也要和StateLess一样了 皆大欢喜啊
但是 和谐么?不和谐! 色块没有发生交换当然不和谐了!
为啥没换??!!!

解释如下:

前文说过
//此处其实是调用的_state的build方法
Widget build() => _state.build(this);

而_state和Statefulelement是一一对应的

此处ElementTree中的Statefulelement位置没有发生改变 那Statefulelement对应的_state还是老的 老的_state颜色是state内部持有的没变 即描述当前Statefulelement的state没变 还是执行老的state的build方法,
你外部交换Widget位置和我Statefulelement有啥关系。。我还执行老的build

总结如下:

1:element在elementtree中没有交换位置
2:element对应的widget发生了交换
3:(element对应的)state的_widget发生了交换
4:执行build执行的是state的build() 但是颜色是跟随state的 所以色块没有发生变化

3:那么问题来了当用示例3给StatefulWidget外部传入color时 为啥色块又能交换了呢?

头疼!!!
明明说StatefulWidget不会变的?!!

哈哈解释如下:
顺着2的解释来:
我还执行老的state中的build(),

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

里面有个widget.defaultColor ,widget是state的_widget

//Stateful的update方法
void update(StatefulWidget newWidget) {
  super.update(newWidget);
  final StatefulWidget oldWidget = _state._widget;
  _dirty = true;
  _state._widget = widget as StatefulWidget;
  rebuild();
}

此处_state的_widget变成了widget widget其实是newWidget;
widget.defaultColor其实是newWidget.defaultColor
噢噢!! 执行build()时都是newWidget的配置了呀 !
所以色块发生了交换

总结如下:

1:element在elementtree中没有交换位置
2:element对应的widget发生了交换
3:(element对应的)state的_widget发生了交换
4:执行build执行的是state的build() 但是颜色是跟随state的widget的 _widget发生了交换 所以颜色发生了交换

额外问题:为啥说element在elementtree中没有交换位置?

官方解释如下:

Update the given child with the given new configuration.
This method is the core of the widgets system. It is called each time we are to add, update, or remove a child based on an updated configuration.
The newSlot argument specifies the new value for this element's slot.
If the child is null, and the newWidget is not null, then we have a new child for which we need to create an Element, configured with newWidget.
If the newWidget is null, and the child is not null, then we need to remove it because it no longer has a configuration.
If neither are null, then we need to update the child's configuration to be the new configuration given by newWidget. If newWidget can be given to the existing child (as determined by Widget.canUpdate), then it is so given. Otherwise, the old child needs to be disposed and a new child created for the new configuration.
If both are null, then we don't have a child and won't have a child, so we do nothing.
The updateChild method returns the new child, if it had to create one, or the child that was passed in, if it just had to update the child, or null, if it removed the child and did not replace it.
The following table summarizes the above:
| | newWidget == null | newWidget != null | | :-----------------: | :--------------------- | :---------------------- | | child == null | Returns null. | Returns new Element. | | child != null | Old child is removed, returns null. | Old child updated if possible, returns child or new Element. |

  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    Element newChild;
    if (child != null) {
      bool hasSameSuperclass = true;
    final int oldElementClass = Element._debugConcreteSubtype(child);
        final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
        hasSameSuperclass = oldElementClass == newWidgetClass;
      if (hasSameSuperclass && child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        newChild = child;
      } else {
        deactivateChild(child);
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else {
      newChild = inflateWidget(newWidget, newSlot);
    }
    return newChild;
  }

当进行交换时打断点可以发现
1:没有创建新的Element
2:逻辑判断都是进更新当前child(Element)而非创建
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
newChild = child;

ElementTree的初始化创建和ElementTree某个Element的更新

flutter底层内核逻辑依然是函数式的

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容