在Widget的构造函数中都有一个选参数key
,例如
Column({
Key key,
...
})
const Text(
Key key,
...
})
那么这个key
作用是什么?事情得从wigdet渲染说起。
wigdet渲染
三个对象
-
Widget
:存放渲染内容、视图布局信息,widget的属性最好都是immutable(如何更新数据呢?查看后续内容) -
Element
:存放上下文,通过Element遍历视图树,Element同时持有Widget和RenderObject -
RenderObject
:根据Widget的布局属性进行layout,paint Widget传人的内容
三棵树
页面在被Flutter引擎渲染时,会有三棵树:wigdet tree、element tree、render tree
。
wigdet tree
:这个就是开发者代码操作的wigdet
,每一个`wigdet都会被加入到当前wigdet tree中。element tree
:在wigdet被构造时,会隐式调用createElement()
方法返回一个对应的Element
,这个Element
会被加入到element tree
中。所以wigdet tree
和element tree
,会有一一对应的关系。同时如果是StatefulElement
那么还会调用createState()
方法创建一个StatefulWidget
的State
的实例。State
实例被StatefulElement
持有。用于后面页面刷新。render tree
: 系统根据element tree
中的element
生RenderObject
,由RenderObject
构成render tree
。但是并不是所有element
都会生成RenderObject
,有些时候,某棵element
子树里面包含了好几个element
,但是只会生个一个RenderObject
。所以render tree
和element tree
结构是不一样
element tree的增量更新
先看 element tree 的更新逻辑源码
List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element> forgottenChildren }) {
...
while (xxx) {
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
final Element newChild = updateChild(oldChild, newWidget, IndexedSlot<Element>(newChildrenTop, previousChild));
...
}
...
return newChildren;
}
其中有一个canUpdate(oldChild.widget, newWidget)
方法。点击去看看
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
啥意思?更新Element tree中的Element需要满足下面条件
- 1.比较oldWidget 和 newWidget是否是同一类型的Widget.
- 2.比较可选参数 key是否一致
这么一看好像也没什么毛病,多重判断,保证element tree更新准确。但是在实际开发中很多时候key是不写的,只会比较oldWidget 和 newWidget是否是同一类型。 如果两者相同,就不需要更新,节省渲染消耗,增加性能。 这样就有可能出现一个问题。我用下面的图来解释。
在一个页面中,某棵子树下面ABC都属于同类型的Widget,此时删除了最前面的A。那么element tree势必也会跟着更新。我们捋下这个element treed 更新过程。
- new widget tree中的剩下的B就会先跟old widget tree中的A比较
- 因为没有A和B都没有设置key,所以只比较两者类型,发现一样。B就代替的A。
- 同理C代替了B。
- 最后C被丢弃了。
- 生成了新的element tree。
有没有发现,原本是想删除A的,却删除了C。所以在日常开发中,同级widget最好填写key
Key的简单介绍
Key 源码
abstract class Key {
/// Construct a [ValueKey<String>] with the given [String].
///
/// This is the simplest way to create keys.
const factory Key(String value) = ValueKey<String>;
/// Default constructor, used by subclasses.
///
/// Useful so that subclasses can call us, because the [new Key] factory
/// constructor shadows the implicit constructor.
@protected
const Key.empty();
}
可以看到其实一个抽象类,并且有个工厂方法。其有两个子类。
- LocalKey,用于diff算法(用于比较element和widget进行比较)。
- ValueKey:以一个数据作为key。如数字、字符
- ObjectKey: 以Object对象作为key
- UniqueKey:返回一个hash值,可以保证key的唯一性。一旦使用了UniqueKey,那么就不存在element的复用了。
- GlobalKey
- 可以获取到应对的Widget对象
Key的使用
1.LocalKey
class StItem extends StatelessWidget {
final title;
StItem(this.title, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container();
}
}
List<Widget> items = [
StItem(
'aaaaa',
key: ValueKey(111),
),
StItem(
'bbbbb',
key: ObjectKey(Text('222')),
),
StItem(
'ccccc',
key: UniqueKey(),
),
];
2.GlobalKey
//GlobalKey的使用! //关于stateFul 尽量在末端!在树的"叶子"处
import 'package:flutter/material.dart';
class GlobalKeyDemo extends StatelessWidget {
final GlobalKey<_ChildPageState> _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GlobalKeyDemo'),
),
body: ChildPage(
key: _globalKey,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
_globalKey.currentState.data =
'old:' + _globalKey.currentState.count.toString();
_globalKey.currentState.count++;
_globalKey.currentState.setState(() {});
},
),
);
}
}
class ChildPage extends StatefulWidget {
ChildPage({Key key}) : super(key: key);
@override
_ChildPageState createState() => _ChildPageState();
}
class _ChildPageState extends State<ChildPage> {
int count = 0;
String data = 'hello';
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: <Widget>[
Text(count.toString()),
Text(data),
],
),
);
}
}