Widget
- 除渲染外的所有类, 常见的有4种
- StatelessWidget
- StatefulWidget
- RendObjectWidget
- InheritedWidget(<font color=green>这里先不说这个类</font>)
其中 <font color=red>RenderObjectWidget是一个抽象类</font>. 在实际开发中基本上使用的都是前2个.
<font color=red>RenderObjectWidget</font>也是一种Widget, 它由用户间接提供渲染的数据给RenderObject
. 如Text再被构造时, 内部的build方法会依赖Text数据再去创建一个RenderObjectWidget的子类, 相当于用户的StatelessWidget所构造的数据间接传递给了RenderObjectWidget
, 最后再合适的时机将数据提供给渲染结点RenderObject
Element
- 由Widget创建, 不同的Widget会创建不同的Element
Widget | Element |
---|---|
StatelessWidget | StatelessElement |
StatefulWidget | StatefulElement |
RendObjectElement | RendObjectElement(<font color=green>抽象类</font>) |
上面所有的Element都是由Widget来创建, 这意味着在开发中, 用户不需要自己创建Element. 并且在运行的过程中. 用户构建不同的Widget, 所创建出来的Element也不一样
RenderObject
- 由
RenderObjectWidget
创建, 它是渲染的结点数据. 在程序的运行过程中, 不同的RenderObjectWidget所创建出来的RenderObject是不一样的.
树
众所周知, 屏幕显示出来的视图在逻辑上是分层的. 这个结构就是渲染树. 树上的每个结点记录着某一层的视图状态. 在Flutter中直接渲染的数据所定义的结构就是
RenderObject
. 它实际上就是树中的结点.
Widget-Tree
- 上述的Widget主要由应用层面的用户来编写. 以 <font color=red>StatelessWidgit为例</font>, 嵌套的形成是由 <font color=red>用户的build方法,逐步递归创建Widget</font>.
同理 <font color=red>StatefulWidget</font>也是一样的.
- 若从这一点来看, 用户编写的所有Widget在逻辑上就是一棵树
这里要提一点: 从代码的调用流程来看, 若用户创建的Widget都是
child
, 则在逻辑上实际上形成的是链表. 但实际开发中用户会创建children
类型的Widget, 这就造成在逻辑上形成的是一棵树结构.
-
Widget-Tree
在开发中会被频繁的创建和销毁(build方法
), 所以若是直接渲染这棵树, 性能会大大下降. 也就是说Widget-Tree
并不适合做渲染的原数据.
Element-Tree
- 由Element对象组成的树结构, 一个Widget必须创建一个Element, 也就是说 <font color=red>用户创建的Widget和Elment是一一对应的</font>.
这里先这样理解, 后面细说它们之间的这种对应是怎么优化的
RenderObject-Tree
- 由 <font color=red>结点构成的树</font>, 这一层是Flutter渲染时真正的状态数据, 这一层在实现上要尽量保持稳定.
- 在Element-Tree中, 有一部分的Element是RenderObjectElement, 所有的RenderObject由这类Element生成, 所以RenderObject-Tree并不和Element-Tree是一一对应的
3种树的关系
- 所有的Widget构成Widget-Tree, 这棵树形成在build的递归调用中. 当调用build后, 仅仅只返回了一个Widget, 此时并不会创建Element, 更不会创建RenderObject
也就是说:
MyWidget1 extends StatelessWidget{
@override
Widget build(BuildContext ctx){
return MyWidget2();
}
}
MyWidget2 extends StatelessWidget{
@override
Widget build(BuildContext ctx){
return MyWidget3();
}
}
MyWidget3 extends StatelessWidget{
@override
Widget build(BuildContext ctx){
return Text("hello");
}
}
对于上面的代码, 当
MyWidget1被构建时, 并不会创建Element
main函数启动加载过程
以下面这个代码为例
void main(List<String> args) {
runApp(Text("hello flutter", textDirection: TextDirection.ltr,));
}
- 创建用户的Widget, 即
Text
, 此时 <font color=red>仅仅是返回Text, 并未创建对应的Element</font>
- 调用runApp函数
一般来说, 窗口程序的启动函数的功能是:1. 向操作系统索要一个窗口; 2. 并开启事件循环
- 创建全局核心对象
WidgetBinding
- 包装用户的Widget(
Text
) - 开始渲染
- 唤醒更新frame
void runApp(Widget app) {
final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
assert(binding.debugCheckZone('runApp'));
binding
..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
..scheduleWarmUpFrame();
}
binding对象负责管理窗口, 事件等. 相当于iOS中的
APPDelegate
scheduleAttachRootWidget
只是准备渲染
- 渲染过程(只展示核心代码)
- 创建
RenderObjectWidget
- 创建
RenderObjectElement
, 并且这个Element为树的根节点
- 创建
void attachRootWidget(Widget rootWidget) {
final bool isBootstrapFrame = rootElement == null;
_readyToProduceFrames = true;
_rootElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
) // 创建RenderObjectWidget
// 返回 RenderObjectElement,
.attachToRenderTree(buildOwner!, rootElement as RenderObjectToWidgetElement<RenderBox>?);
if (isBootstrapFrame) {
SchedulerBinding.instance.ensureVisualUpdate();
}
}
这一整个函数的的结果是返回了一个
rootRenderObjectElement
. 内部的widget为最开始的被包装的Text
,renderObject为全局对象的containerView
- 返回rootElement的过程
- 创建
RenderObjectElement
- 引用全局的BuildOwner
- 挂载
- 创建
// 当前对象是 RenderObjectToWidgetAdapter, 也就是 RenderObjectWidget
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>element ]) {
if (element == null) {
owner.lockState(() {
element = createElement(); // 创建rootElement, 并且 绑定widget为this
assert(element != null);
element!.assignOwner(owner);
});
owner.buildScope(element!, () {
element!.mount(null, null); // 最核心的挂载
});
// 下面的不看
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element!;
}
挂载是Element的核心, 每一种不同子类的Element的挂载效果是不一样的. 比如
StatelessElement和StatefulElement
所实现的挂载是要调用build
, 然后更新 子Element的widget. 这中间的动作会造成递归调用所有build方法得到子widget, 并依次创建对应的Element
RenderObjectElement
的挂载只是更新子Element
Element的mount公用处理
- 上面的mount是RenderObjectElement的, 对于公用的父类Element, 有所有Element的mount的公共处理
// this当前是Element, 要将 this挂载到指定的 parent中
void mount(Elementparent, ObjectnewSlot) {
assert(_lifecycleState == _ElementLifecycle.initial);
assert(_parent == null);
assert(parent == null || parent._lifecycleState == _ElementLifecycle.active);
assert(slot == null);
// 若当前 this的状态不是初始化, 则异常
// 若当前 this已经有 parent了, 则异常
// 若当前 指定的parent为空, 则表示this是根据节点
// 或 指定了parent, 但自己的状态已经是active了, 也是异常
// 若当前 this的槽不为空, 则异常
// 以上断言的总结就是: 自己是刚被初始化状态时, 才可以被挂载
/// 初始化时, 自己一定没有槽, 并且一定没有 parent
_parent = parent;
_slot = newSlot;
_lifecycleState = _ElementLifecycle.active;
_depth = _parent != null _parent!.depth + 1 : 1;
if (parent != null) {
_owner = parent.owner;
}
// 以上是为 this绑定 父结点和槽
// 指定自己的深度
// 引用全局的 BuildOwner对象
assert(owner != null);
final Keykey = widget.key;
if (key is GlobalKey) {
owner!._registerGlobalKey(key, this);
}
// 这一步暗示了 GrobalKey的作用
// 即若指定的 Widget的Key的类型是GlobalKey,则会被全局引用
// 它是 GlobalKey共享数据的原因
_updateInheritance();
attachNotificationTree();
// 这2步这里先不说
}
以上是所有Element在挂载时要做的相同动作
RenderObjectElement的挂载过程
- 这一步的挂载就是接着runApp的代码所来
- 调用公共处理(
Element.mount
) - 创建
RenderObject
- 临时的为当前的 this 生成节点信息, 但最后要不要挂载, 要在后面做虚拟DOM的diff算法
- 调用公共处理(
void mount(Elementparent, ObjectnewSlot) {
super.mount(parent, newSlot);
assert(() {
_debugDoingBuild = true;
return true;
}());
_renderObject = (widget as RenderObjectWidget).createRenderObject(this); // __code_0
assert(!_renderObject!.debugDisposed!);
assert(() {
_debugDoingBuild = false;
return true;
}());
assert(() {
_debugUpdateRenderObjectOwner();
return true;
}());
assert(_slot == newSlot);
// 内部会判断要不要挂载, 虚拟dom的diff算法
attachRenderObject(newSlot);
super.performRebuild(); // clears the "dirty" flag
}
// 该函数虽然是override, 但事实上 RenderObjectElement并未调用 Element的实现, 对于
// RenderObjectElement attachRenderObject函数自己的操作
@override
void attachRenderObject(ObjectnewSlot) {
assert(_ancestorRenderObjectElement == null);
// 当前 renderObjectElement对象的 槽(slot)被更新为指定的newSlot
_slot = newSlot;
// 离 this(RenderObjectElement)最近的 祖先结点
// 即在 Element-Tree中通过 parent往上找, 找到最近的 RenderObjectElement的结点
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
// 找到最近的 祖先(RenderObjectElement)后, 将 this.renderObject挂载到 祖先的renderObject的child下
// 这一步相当于在 操作 RenderObject-Tree(第3层)
// 并且也可以得出这样一个结论, RenderObjectElement是Element-Tree中的一部分
_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
// 因为上面会改变 RenderObject-Tree(插入了新结点)
// 这里根据官方说的, 要更新 最近祖先的数据, 这里的祖先和_ancestorRenderObjectElement 不同
// 它所表示的类型是 ParentDataElement, 也是RenderObject-Tree中的RenderObject结点
// 这里查找的结果parentDataElement 可能等于_ancestorRenderObjectElement
// 也可能是_ancestorRenderObjectElement的祖先结点
final ParentDataElement<ParentData>parentDataElement = _findAncestorParentDataElement();
if (parentDataElement != null) {
// 所以parentDataElement.widget一定是 RenderObjectWidget(用户提供的渲染数据)
_updateParentData(parentDataElement.widget as ParentDataWidget<ParentData>);
}
}
__code_0
在创建RenderObject, 由Widget创建. 换句话说:RenderObject是由RenderObjectWidget所创建
上面2个函数总结就是: RenderObjectWidgetElement通过引用的RenderObjectWidget创建渲染结点(RenderObject), 并将创建的结点附加到RenderObject-Tree
中, 当添加到渲染树中时, 会更新最近的父结点的数据
它为什么要更新父结点的数据以一个场景来说: 若在树中插入了一个新的结点(如用户提供了RichText后, 根据流程会创建RenderObject)后, 当前节点往上的部分可能是Flex布局在操作, 新节点的可能会引起Flex要重新计算,并排布界面, 所以要在当前结点上往上找到父节点, 然后重新计算布局数据
ps: <font color = red>所以更新父节点的后续操作中, 所谓的父节点可能是Flex, 可能是Positioned等, 它们要重新布局</font>
RenderObjectToWidgetElement的mount
- 上面所有的mount所指向的this事实上是
RendObjectToWidgetElement
对象- 调用父类的mount
- _rebuild
void mount(Elementparent, ObjectnewSlot) {
assert(parent == null);
super.mount(parent, newSlot);
_rebuild();
assert(_child != null);
}
void _rebuild() {
try {
// 这里更新子Element
_child = updateChild(_child, (widget as RenderObjectToWidgetAdapter<T>).child, _rootChildSlot);
} catch (exception, stack) {
...
}
}
</br/>
Element更新子Element过程<a id="elemnt-update"/>
- 该方法是Element-Tree实现虚拟DOM的核心, 方法如下:
ElementupdateChild(Elementchild, WidgetnewWidget, ObjectnewSlot) {
// 若当前要更新的 子element(child) 没有指定widget(newWidget)则表示
// 当 子element没有对应widget,即直接从 Element-Tree删除
// 但 child 只是从Element-Tree中删除, 它会被记录到 BuildOwner不活动的map中
if (newWidget == null) {
if (child != null) {
deactivateChild(child);
}
return null;
}
final Element newChild;
if (child != null) {
// 它表示的意义是: 当前this的子element(child) 的类型 是否为 widget所对应的element类型
// 即 StatelessWidget应该对应StatelessElement
// StatefulWidget应该对应StatefulElement
// 这样的意义是 若 子element(child) 和 widget(newWidget) 类型对应时
//// 会直接在 子element(child)下作更新操作, 然后 直接返回 element
//// 若类型不对应, 则表示原来 child可能是 StatelessElement, 现在对应要更新的widget(newWidget)却为 StatefulWidget, 则要重新删除 child, 并创建新的Element, 然后会重新mount
bool hasSameSuperclass = true;
/// 这一段的断言的作用是: 在debug模式下热重载时
/// 用户可能直接 更改了 build中前后 类型不同的 Widget
//// 如 热重载前是 StatefulWidget
//// 然后用户修为 StatelessWidget
//// 然后按下热重载 element的widget指向的还是StatefulWidget, 则可能会crash, 所以这里在debug热重载作一个断言, 时时判断前后的类型是不是不一致
/// ps: 在release下, 没有热重载功能, 所有的东西都是编译好的, 外界调用该函数都是 element.updateChild(element.child, element.widget.child, slot) 不会出现 element.child的类型 与 element.widget.child不对应问题
assert(() {
final int oldElementClass = Element._debugConcreteSubtype(child);
final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
hasSameSuperclass = oldElementClass == newWidgetClass;
return true;
}());
// 若类型对应, 并且 子element前后的widget并未改变
/// 则只更新槽后直接返回
if (hasSameSuperclass && child.widget == newWidget) {
if (child.slot != newSlot) {
updateSlotForChild(child, newSlot);
}
newChild = child;
// 若类型对应, 但不是同一个widget, 但它们的key相等Widget.canUpdate, 则也是更新槽, 同时要替换 child的widget
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot) {
updateSlotForChild(child, newSlot);
}
... 其他不相关的代码
child.update(newWidget); // 替换 child的widget
... 其他不相关的代码
newChild = child;
} else {
/// 类型不对应 或对应但它们的key不同, 则将 child从Element-Tree中删除
/// 然后创建新的 newChild, 后续的过程见单独的inflateWidget探究
deactivateChild(child);
assert(child._parent == null);
newChild = inflateWidget(newWidget, newSlot);
}
} else {
/// child为空时, 直接创建新的 Element
newChild = inflateWidget(newWidget, newSlot);
}
.... 其他不相关代码
return newChild;
}
这里所谓的diff算法, 本质是通过前后Widget的类型和key是不是一样的(
Widget.canUpdate
), 若一样, 则不创建Element
updateChild时创建子Element(inflateWidget
)
- 该方法由Element实现, 也有对应的子类, 这里看公共处理
Element inflateWidget(Widget newWidget, ObjectnewSlot) {
... 其他不相关代码
// 若当newWidegt是一个全局对象, 即它的key为GlobalKey类型 从全局缓存中取
//// 有一个全局map, key为GlobalKey对象, value是该对象引用的Element
// 若取到Global对象, 则找到对应的element则并挂载到this(_activateWithParent)下
// 以取到的
try {
final Keykey = newWidget.key;
if (key is GlobalKey) {
final ElementnewChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
newChild._activateWithParent(this, newSlot);
final ElementupdatedChild = updateChild(newChild, newWidget, newSlot);
return updatedChild!;
}
}
// 若newWidget不是全局对象, 则直接通过它来创建新的element
final Element newChild = newWidget.createElement();
// 将新element挂载到 当前elmement中
// 此时因为Widget可能是不同类型的Widget, 所以调用mount方法后
// 可能具体的实现也就不同,
newChild.mount(this, newSlot); // __code_0
return newChild;
} finally {
if (isTimelineTracked) {
Timeline.finishSync();
}
}
}
__code_0
表示新element的挂载, 从main开始到这里后, 方法中的newWidget
实际就是最开始的被包装的Text
, 所以这里实际调用的是StatelessElement.mount
, 被包装的Text类型是StatelessWidget
, 所以后面实际调用的代码如下:
// 当前this是 ComponentElement
void mount(Elementparent, ObjectnewSlot) {
super.mount(parent, newSlot);
assert(_child == null);
assert(_lifecycleState == _ElementLifecycle.active);
_firstBuild(); // 第1次build
assert(_child != null);
}
// build最后会调用到 ComponentElement.performRebuild
void performRebuild() {
Widgetbuilt;
try {
..
// 这里就是调用 最常见的 用户的 build方法, 返回Widget
built = build();
} catch (e, stack) {
...
} finally {
super.performRebuild(); // clears the "dirty" flag
}
try {
// 拿到 built(Widget)后, 将它更新到 Element(_child)中
// updateChild后面的过程就会又根据情况创建 built对应的Element对象
/// 又调用mount, 然后就形成的递归
_child = updateChild(_child, built, slot);
assert(_child != null);
} catch (e, stack) {
...
}
}
至此main函数基本流程已经完毕, 总结一点就是:
- 最先调用用户的Widget的构造方法
- 创建根结点
- 由根结点(
RenderObjectWidget
)创建对应的RenderObjectElement
- 开始mount
- mount先形成树中的结点关系, 再更新子Element
- 更新子Element的过程, 会做各种优化, 如diff, 如要不要创建子element
- 若第6步创建了子element, 则又会调用对应的mount, 而若是用户的element(
StatelessWidget等
)就会调用到build方法- 再根据第7步build得到的Widget, update子Element, 回到第6步形成树的递归调用
StatefulWidget
- 如上面的分析, 当调用到
mount
时, 若当前的Element是StatefulElement, 则发生的动作有所不同
@override
void _firstBuild() {
...
// 生命周期函数 initState
final ObjectdebugCheckForReturnedFuture = state.initState() as dynamic;
...
// 生命周期函数 didChangeDependencies
state.didChangeDependencies();
..
super._firstBuild();
}
很明显, 通过引用的
State<StatefulWidget>
调用对应的生命周期函数
通过父类的build再调用到State的build方法
StatefulWidget的注意点1
- 在开发中, 很多种情况下都会使用StatefulWidget, 因为业务的问题在适当的时间点可能会更新状态.
- StatefulElement中在进行到build时, 调用的是
引用的state
的build方法. 当用户调用刷新状态(setState
)的方法时, state并不会重建, 重建的只是widget. 所以在某些场景下就要注意一些问题
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
void main(List<String> args) {
return runApp(MaterialApp(home: Scaffold(body: HttpDemo1("init"),appBar: AppBar(title: Text("hello"),),),));
}
class HttpDemo1 extends StatefulWidget {
String _data;
HttpDemo1(String data):_data = data;
@override
State<HttpDemo1> createState() => _HttpDemo1State();
}
class _HttpDemo1State extends State<HttpDemo1> {
final bgc = Colors.red;
@override
void initState() {
super.initState();
Timer(Duration(seconds: 5), () {
setState(() {
this.widget._data = "change";
});
});
}
@override
Widget build(BuildContext context) {
return Container(child: Text(this.widget._data),height: 50, width: double.infinity, decoration: BoxDecoration(color: this.bgc),);
}
}
class MYItem extends StatefulWidget {
final String name;
const MYItem({this.name = ""});
@override
State<MYItem> createState() => _MYItemState();
}
class _MYItemState extends State<MYItem> {
@override
Widget build(BuildContext context) {
return Container(color: Color(Random().nextInt(0x100000000)), child: Text(this.widget.name), alignment: Alignment.center, width: double.infinity, height: 50,);
}
}
上述程序中, 过了5秒后, 调用刷新方法时, 修改了数据, 最后渲染时, state并未重建, 但container重建了, 但前后重建的背景颜色却始终是红色.
下面来看看setState
后发生了什么
setState方法实现
- setState提供了一个回调方法, 由Flutter内部调用
void setState(VoidCallback fn) {
... 其他不相关的代码
final Objectresult = fn() as dynamic; // 回调用户的操作
... 其他不相关的代码
_element!.markNeedsBuild(); // 里面会标记 当前StatefulElement要刷新, 此时的element是 StatefulElement
}
标记刷新的方法
// this是StatefulElement
void markNeedsBuild() {
... 不相关的代码
if (dirty) { // 若当前 已经标记了, 则直接返回
return;
}
_dirty = true;
owner!.scheduleBuildFor(this); // 这里面有核心的一步, 标记 element.dirty = true;
// true的意义表示要build, 但不是当前立即build, 而是等待系统的drawFrame的回调()
// 并将自己添加到 buildOwner._dirtyElements的数组中, 这样只用遍历该数组中的记录去作对应的布局刷新
/// 效率就更高
}
这里只简单描述: 当调用
setState
后 1.Flutter标记elment.dirty = true; 2. 添加到buildOwner.dirtyElements数组中. 然后等待系统的drawFrame
的回调, 后续调用到buildOwner.buildScope(_rootElement)
, 该函数内部取到buildOwenr.dirtyElements
数组, 一个一个调用elment相关的build方法. 而对于StatefulElement来说, build意味着不会创建State
Element-Tree中的element
- 前面说过, element树结构相对
Widget-Tree
要稳定, 所以从实现上来看树中的结点element
void main(List<String> args) {
return runApp(MaterialApp(home: Scaffold(body: HttpDemo2(),appBar: AppBar(title: Text("hello"),),),));
}
class HttpDemo2 extends StatefulWidget {
const HttpDemo2({super.key});
@override
State<HttpDemo2> createState() => _HttpDemo2State();
}
class _HttpDemo2State extends State<HttpDemo2> {
@override
Widget build(BuildContext context) {
return Column(
children: [
TestItem2(show_str: DateTime.timestamp().toString(),),
TextButton(onPressed: (){
setState(() {
});
}, child: Text("点我"))
],
);
}
}
class TestItem2 extends StatelessWidget {
final String _show_str;
TestItem2({show_str,super.key}): _show_str = show_str{
print("construct:${this.runtimeType}--${this.hashCode}");
}
@override
Widget build(BuildContext context) {
print("widget-type:${context.widget.runtimeType}\t\twidget-code:${context.widget.hashCode}\t\tcontext-code${context.hashCode}");
return Container(width: 300, height: 300, color: Colors.red, child: Text(this._show_str),);
}
}
测试结果
Performing hot restart...
Syncing files to device iPhone 14 Pro Max...
Restarted application in 293ms.
flutter: construct:TestItem2--247503628
flutter: widget-type:TestItem2 widget-code:247503628 context-code496134088
flutter: construct:TestItem2--313661942
flutter: widget-type:TestItem2 widget-code:313661942 context-code496134088
flutter: construct:TestItem2--760305741
flutter: widget-type:TestItem2 widget-code:760305741 context-code496134088
flutter: construct:TestItem2--114051490
flutter: widget-type:TestItem2 widget-code:114051490 context-code496134088
flutter: construct:TestItem2--1004922686
flutter: widget-type:TestItem2 widget-code:1004922686 context-code496134088
flutter: construct:TestItem2--925506391
flutter: widget-type:TestItem2 widget-code:925506391 context-code496134088
可以发现, 不管点击按钮更新多少次, Element-Tree中的一个element不会有变动, 但它所绑定的widget在不断的创建和销毁. 并且在调用build前就已经绑定了. 这一点在前面的源码探索中已经看过了
StatefulWidget注意点2
- 在StatefulWidget中的State调用
setState
后, 对应的state
在build时发现上下文没有变(StatelessElement
), 那这个elment中的state
属性改变了没有呢?前面可能没有细说到, StatefulWidget在创建element时,创建的类型是
StatefulElement
, 这个element在被创建时,会指定state引用到StatefulWidget中的state对象
- 因为
state
void main(List<String> args) {
return runApp(MaterialApp(home: Scaffold(body: HttpDemo1(),appBar: AppBar(title: Text("hello"),),),));
}
class HttpDemo1 extends StatefulWidget {
@override
State<HttpDemo1> createState() => _HttpDemo1State();
}
class _HttpDemo1State extends State<HttpDemo1> {
final List datas = ["hello"];
@override
Widget build(BuildContext context) {
return Column(children: [
...this.datas.map((e){
return MYItem(name: e);
}),
TextButton(onPressed: (){
setState(() {
this.datas[0] = DateTime.timestamp().toString(); // __code_0
});
}, child: Text("点我"))
],);
}
}
class MYItem extends StatefulWidget {
final String name;
const MYItem({this.name = ""});
@override
State<MYItem> createState() => _MYItemState();
}
class _MYItemState extends State<MYItem> {
final bgc = Color(Random().nextInt(0x100000000));
@override
void initState() {
super.initState();
print("initstate:${this.runtimeType}----${this.hashCode}");
}
@override
Widget build(BuildContext context) {
print("${context.runtimeType}---------${context.hashCode} --- ${(context as StatefulElement).state}"); // __code_1
var result = Container(color: this.bgc, child: Text(this.widget.name), alignment: Alignment.center, width: double.infinity, height: 50,);
return result;
}
上述程序其实很简单, 就是改变父State时(
__code_0
), 重新构建子StatefulWidget时, element下绑定的state有没有重建(__code_1
). 测试结果如下:
flutter: StatefulElement---------647177149 --- _MYItemState#89d6a
flutter: StatefulElement---------647177149 --- _MYItemState#89d6a
flutter: StatefulElement---------647177149 --- _MYItemState#89d6a
flutter: StatefulElement---------647177149 --- _MYItemState#89d6a
flutter: StatefulElement---------647177149 --- _MYItemState#89d6a
flutter: StatefulElement---------647177149 --- _MYItemState#89d6a
flutter: StatefulElement---------647177149 --- _MYItemState#89d6a
flutter: StatefulElement---------647177149 --- _MYItemState#89d6a
说明
setState
后, 其子节点下的StateElemet并不会重新创建state, 从这一点来看, Flutter内部的确是在保持Element-Tree的稳定. 从视图层面来说, 就是在复用视图. 但在程序不断运行的过程中, StatefulWidget可能在不断的被重建, 所以Flutter必定会做 新Widget和旧Widget的对比, 然后对Element-Tree的节点做更新操作
这一点已经在结点更新的源码中跟踪了
复用的一个demo
import 'dart:math';
import 'package:flutter/material.dart';
void main(List<String> args) {
return runApp(MaterialApp(home: Scaffold(body: HttpDemo1(),appBar: AppBar(title: Text("hello"),),),));
}
class HttpDemo1 extends StatefulWidget {
@override
State<HttpDemo1> createState() => _HttpDemo1State();
}
class _HttpDemo1State extends State<HttpDemo1> {
final List datas = ["hello", "world", "nice"];
@override
Widget build(BuildContext context) {
return Column(children: [
...this.datas.map((e){
return MYItem(name: e); // _code_key
}),
TextButton(onPressed: (){
setState(() {
this.datas.removeAt(0);
});
}, child: Text("点我"))
],);
}
}
class MYItem extends StatefulWidget {
final String name;
const MYItem({this.name = "",super.key});
@override
State<MYItem> createState() => _MYItemState();
}
class _MYItemState extends State<MYItem> {
final bgc = Color(Random().nextInt(0x100000000));
@override
Widget build(BuildContext context) {
return Container(color: this.bgc, child: Text(this.widget.name), alignment: Alignment.center, width: double.infinity, height: 50,);
}
}
该程序测试了视图复用的一个问题:
删除数组中的第0个元素后, 由于state未重新创建, 所以颜色是对不上的, 自己可以测试一下
假设初始化的状态是:
红 --> hello
绿 --> world
蓝 --> nice
第1次点击按钮后, 结果是:
红 --> world
绿 --> nice
第2次点击后:
红 --> nice
想要的效果是:
第1次后:
绿 --> world
蓝 --> nice
第2次后:
蓝 --> nice
原因:
1. setState
2. 标记 dirty = true, 并添加到 buildOwner.dirtyElements, 其实添加的是 StatefulElement
3. 等待系统的回调, 然后到 buildOwner.buildScope(_rootElement)被回调
4. 再通过 dirtyElements的数组取到 _HttpDemo1State对应的StatefulElement
5. 调用 _HttpDemo1State._rebuild, 再调用到 build
6. 获取到 build返回的新 Column的Widget, Column的children是2个 _MyItemState
7. 然后调用 StatefulElement.updateChild方法, 其中this就是_HttpDemo1State对应的elment, 传递的参数是:
> child --> 就是已经存在Element-Tree中的Column对应的elment
> newWidget-> 第6步返回的 新 Column
8. 在updateChild中:
因为 child和newWidget的类型是对应的, 并且child.widget的key和newWidget的key也是一样的
所以并不会创建新的 Column对应的element
但会重新绑定 child的widget为newWidget
9. 第8步会将 child的widget更新为 newWidget,
>child.update(newWidget);
10. 因为child对应的Element实际是Column所对应的Element, 即MultiChildRenderObjectWidget, 所以会调用到该类的实现中
11. 第10步的实现中比较复杂, 但实现的结果就是上面演示的结果
内部其实是在作 新旧比较, 从左到右遍历旧的, 然后比对新的
解决上面的方案是在创建MyItem时指定key, 这样在第11步比对过程中, 就不是单纯的从左到右, 还要看key是不是一样的, 若是一样的才会复用, 即将
__code_key
这一行改为
return MYItem(name: e, ValueKey(e)); // _code_key