接触过Flutter的同学肯定对Key并不陌生,我们在所有Widget中都可以看到构造函数中有一个命名参数key,比如在StatefulWidget
中:
abstract class StatefulWidget extends Widget{
/// Initializes [key] for subclasses.
const StatefulWidget({ Key? key }) : super(key: key);
...
}
那么我相信你肯定会问这个key
是用来做什么的?为何要放一个看似无用的参数在这里呢?今天我们就来一探究竟。
下面是一个🌰
接下来我们仔细看看下面这个例子。关门,放代码😏
// 创建一个页面,中间放三个色块,每点击一次按钮,删除_containers数组中第一个元素
// 请仔细观察色块的变化
class _MyHomePageState extends State<MyHomePage> {
final List<AAContainer> _containers = [
const AAContainer('A'),
const AAContainer('B'),
const AAContainer('C'),
];
void _updateColor(){
_containers.removeAt(0);
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('key demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _containers
),
),
floatingActionButton: FloatingActionButton(
onPressed: _updateColor,
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
AAContainer
代码如下:
class AAContainer extends StatefulWidget {
final String title;
const AAContainer(this.title,{Key? key}) : super(key: key);
@override
_AAContainerState createState() => _AAContainerState();
}
class _AAContainerState extends State<AAContainer> {
final Color _color = Color.fromRGBO(Random().nextInt(256),Random().nextInt(256),Random().nextInt(256),1);
@override
Widget build(BuildContext context) {
return Container(
height: 100,
width: 100,
color: _color,
child: Center(
child: Text(widget.title),
),
);
}
}
运行结果如下:
等等,先让我们对这几个颜色的认知达成一致:
- A色块:红色
- B色块:蓝色
- C色块:紫色
我们对上面代码的预想结果应该是 点击第一下按钮的时候红色色块消失, 点击第二下按钮蓝色色块消失, 点击第三下按钮紫色色块消失, 如果是这样,那就太完美了,但是真的是这样的吗 ?请看下面的gif
诶?咋回事。。。怎么不按套路来呢?要搞清楚这个问题,我们就需要去研究一下Flutter是如何实现页面刷新的
页面刷新原理
总所周知,Flutter采用增量更新的方式来刷新页面,那么它是如何判断那部分发生变化的呢?
setState
我们知道要让StatefulWidget
刷新,我们需要调用setState
方法,那么这个方法中做了什么,接下来我们从这个切入点来突破。
setState
源码
@protected
void setState(VoidCallback fn) {
//无关代码已略去
_element!.markNeedsBuild();
}
在setState
中只调用了markNeedsBuild
这一个方法,我们进入到markNeedsBuild
:
markNeedsBuild
void markNeedsBuild() {
//无关代码已略去
if (_lifecycleState != _ElementLifecycle.active)
return;
if (dirty)
return;
_dirty = true;
// owner是BuildOwner的实例对象,this是当前element
owner!.scheduleBuildFor(this);
}
markNeedsBuild
先将当前element
标记为dirty,然后将this(当前element)
加入到渲染队列中,接下来我们康康scheduleBuildFor
这个方法:
scheduleBuildFor
void scheduleBuildFor(Element element) {
//断言代码已略去
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled!();
}
_dirtyElements.add(element);
element._inDirtyList = true;
}
这个方法只是将当前element
加入到渲染队列中,并且标记element
已经在队列中。这个时候我们就懵逼了,难道到这里就结束了吗 ?通过查看源码我们发现在BuildOwner
还有另外一个方法buildScope
,通过调试我们发现每次调用setState
都会来到这里,我们再来康康这部分的源码:
buildScope
void buildScope(Element context, [ VoidCallback? callback ]) {
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
_dirtyElements[index].rebuild();
}
}
我们发现这段代码是将scheduleBuildFor
中加入到_dirtyElements
中的element取出来并调用rebuild
方法,我们再来查看rebuild
源码:
rebuild
void rebuild() {
//断言代码已略去
performRebuild();
}
StatefulWidget
所对应的element
是StatefulElement
,StatefulElement
又继承自ComponentElement
所以我们直接查看ComponentElement
的performRebuild
方法:
performRebuild
void performRebuild() {
// 略去了很多判断代码
built = build();
_child = updateChild(_child, built, slot);
}
😤这样找下去是不是很累? 哈哈,别急,马上就结束了。接下来我们来到updateChild
方法:
updateChild
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
//如果在widgetTree中当前widget被删除则直接结束,并在ElementTree中也删除它
if (newWidget == null) {
deactivateChild(child);
return;
}
...
Element newChild;
if (child != null) {
...
if (hasSameSuperclass && child.widget == newWidget) {
//如果相同则不进行updata操作
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
//如果可以更新则进行update操作
child.update(newWidget);
newChild = child;
}else{
newChild = inflateWidget(newWidget, newSlot);
}
}
}
通过源码我们可以看出,再调用update
方法之前,会通过canUpdate
来判断是否可以更新,那么我们来看看canUpdate
如何实现:
canUpdate
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
这段代码我相信都看的懂,上面的demo中的AAContainer
的runtimeType
是一致的,key
也是一致的,所以会调用element
的update
方法,但是在update
方法中仅仅只是将当前element的widget指向了新的widget,也就是说把红色element
指向了蓝色的widget
update
void update(covariant Widget newWidget) {
_widget = newWidget;
}
也就是说更新后,红色element
还是在element树中,但是红色element.widget
却指向了蓝色widget
,才会导致上面demo中的错误。
那么这个问题怎么解决呢?
解决方案
我们从上面的分析得出因为调用了update
方法导致结果出错,那么我们有没有什么方法能让element
不去调用update呢?肯定是有的,那就是我们今天讨论的主题Key
。我们对🌰做个改造:
final List<AAContainer> _containers = [
const AAContainer('A',key: ValueKey('A'),),
const AAContainer('B',key: ValueKey('B'),),
const AAContainer('C',key: ValueKey('C'),),
];
只是在创建AAContainer的时候加上了参数key,我们看看结果:
怎么样,是不是完美了😁😁😁
Key的分类
我们已经知道了key是如何工作的,接下来给大家介绍一下Key的分类:
LocalKey
LocalKey
是Diff
算法的核心所在,用作Element和Widget进行比较。
以下几种Key也继承自LocalKey:
- ValueKey: 以任何类型作为key
- ObjectKey: 以Object对象作为Key
- UniqueKey: 保证了Key的唯一性,一旦使用UniqueKey,那么将不存在element复用
GlobalKey
GlobalKey
可以获取到对应Widget的State
对象,我们通过一个🌰 来了解GlobalKey
的用法:
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),
],
),
);
}
}
上面的🌰 中我们在父widget
中更新了子widget
的count值,你就说秒不妙吧!
再见,结束!