今天做了一个类似抖音的app的时候,当点赞删除的时候,总是出现多个item重复出现的情况,于是才发现key的作用如此之大。所以今天给大家讲讲key的原理到底是什么。
key的使用场景只有一个,当你想给一系列相同类型的并有各种不同状态的widget进行添加,删除,排序的时候会使用到
一、Key的分类
Key是所有key的父类。且主要有两类key,即GlobalKey和LocalKey
二、key的使用场景
GlobalKey(全局key)
-
主要用于对widget的更新(局部更新),获取widget的大小,获取widget父widget的属性,获取widget的element
LocalKey(局部Key)
- UniqueKey:主要用于动画的刷新。或用于需要每次都要重建刷新widget的场景
- ObjectKey(value):传入的是对象的引用,通过比较当前位置对象引用或类型是否相同进行更新,一般用于位置调换。或列表更新。
- ValueKey(value):传入的是值,通过比较该值是否相等或类型是否相同进行判断更新,同ObjectKey,主要区别是比较的值不同
三、GlobalKey使用原理
class GlobalKey {
//从注册表中获取到绑定到element
Element? get _currentElement => WidgetsBinding.instance.buildOwner!._globalKeyRegistry[this];
//获取context即element
BuildContext? get currentContext => _currentElement;
//获取绑定到widget
Widget? get currentWidget => _currentElement?.widget;
//获取statefulWidget的state状态
T? get currentState {
final Element? element = _currentElement;
if (element is StatefulElement) {
final StatefulElement statefulElement = element;
final State state = statefulElement.state;
if (state is T) {
return state;
}
}
return null;
}
}
//由BuildOwner持有全局的globalkey对应的element
class BuildOwner {
final Map<GlobalKey, Element> _globalKeyRegistry = <GlobalKey, Element>{};
void _registerGlobalKey(GlobalKey key, Element element) {
//注册globalkey
_globalKeyRegistry[key] = element;
}
void _unregisterGlobalKey(GlobalKey key, Element element) {
//解除globalkey
if (_globalKeyRegistry[key] == element) {
_globalKeyRegistry.remove(key);
}
}
}
//element,element第一次挂载的时候对globalkey的第一次注册
class Element {
//第一次挂载element时注册globalkey
void mount(Element? parent, Object? newSlot) {
final Key? key = widget.key;
if (key is GlobalKey) {
owner!._registerGlobalKey(key, this);
}
}
//element卸载时同时移除globalkey
void unmount(Element? parent, Object? newSlot) {
final Key? key = widget.key;
if (key is GlobalKey) {
owner!._unregisterGlobalKey(key, this);
}
}
}
GlobalKey流程
- widget在第一次挂载mount的时候会使用全局的owner渲染通道对象将key对应的element进行保存
- widget在卸载unmount的时候再使用owner对象将key对应的element进行移除操作
- widget对应的element保存在全局BuildOwner的_globalKeyRegistry中。
//新widget的第一次加载
Element inflateWidget(Widget newWidget, Object? newSlot) {
try {
final Key? key = newWidget.key;
//判断如果是GlobalKey则尝试获取对应的element
if (key is GlobalKey) {
//获取globalkey对应的element
final Element? newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
try {
newChild._activateWithParent(this, newSlot);
} catch (_) {
try {
deactivateChild(newChild);
} catch (_) {
// Clean-up failed. Only surface original exception.
}
rethrow;
}
//如果element存在,则基于该element进行下一步的更新
final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
return updatedChild!;
}
}
//如果通过globalKey获取的element不存在则进行element的创建,继续往下走
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
return newChild;
} finally {
}
}
Element? _retakeInactiveElement(GlobalKey key, Widget newWidget) {
//从BuildOwner的注册表中取出element
final Element? element = key._currentElement;
if (element == null) {
return null;
}
//一般key相同类型即是相同
if (!Widget.canUpdate(element.widget, newWidget)) {
return null;
}
final Element? parent = element._parent;
if (parent != null) {
parent.forgetChild(element);
parent.deactivateChild(element);
}
//从卸载列表中移除,避免被回收
owner!._inactiveElements.remove(element);
return element;
}
void deactivateChild(Element child) {
child._parent = null;
child.detachRenderObject();
owner!._inactiveElements.add(child);
}
void _activateWithParent(Element parent, Object? newSlot) {
_parent = parent;
_updateDepth(_parent!.depth);
_activateRecursively(this);
attachRenderObject(newSlot);
}
static void _activateRecursively(Element element) {
element.activate();
element.visitChildren(_activateRecursively);
}
GlobalKey使用流程
- 在widget的第一次加载的时候调用了inflateWidget,并通过_retakeInactiveElement获取注册表中GlobalKey对应的element。
- 如果获取的element为空则新建一个element,并进行element的挂载
- 如果element不为空则调用_activateWithParent先对父element进行绑定持有,并通过_updateDepth更新父element的depth值,方便后续更新使用。
- 再调用自身element的_activateRecursively方法递归调用activate。
void activate() {
assert(_lifecycleState == _ElementLifecycle.inactive);
assert(owner != null);
final bool hadDependencies = (_dependencies != null && _dependencies!.isNotEmpty) || _hadUnsatisfiedDependencies;
_lifecycleState = _ElementLifecycle.active;
//清空当前element持有的依赖列表
_dependencies?.clear();
_hadUnsatisfiedDependencies = false;
//重新从父element中获取新的依赖列表
_updateInheritance();
attachNotificationTree();
if (_dirty) {
owner!.scheduleBuildFor(this);
}
//如果有依赖项,则执行相关的state的生命周期函数didChangeDependencies()
if (hadDependencies) {
didChangeDependencies();
}
}
///将element加入到_dirtyElements脏列表,等待vsync请求到来的时候进行更新相关element
void scheduleBuildFor(Element element) {
}
//注意这里,递归调用每个子element的_activateRecursively,即_activateRecursively中的activate方法
@override
void visitChildren(ElementVisitor visitor) {
if (_child != null) {
visitor(_child!);
}
}
activate:主要是处理依赖关系,更新依赖列表,并调用didChangeDependencies的生命周期函数(如果有依赖)
visitChildren:递归调用子element的activate方法更新依赖列表等。
四、LocalKey使用原理
localKey是局部key,作用于该节点下的各个子节点,所以一般与MultiChildRenderObjectElement这个element有关
所以我们先从MultiChildRenderObjectElement的update更新子element开始分析
void update(MultiChildRenderObjectWidget newWidget) {
super.update(newWidget);
final MultiChildRenderObjectWidget multiChildRenderObjectWidget = widget as MultiChildRenderObjectWidget;
//这里即将开始子节点的更新逻辑。
_children = updateChildren(_children, multiChildRenderObjectWidget.children, forgottenChildren: _forgottenChildren);
//这里的_forgottenChildren存储的就是使用GlobalKey的element。清除GlobalKey相关的element
_forgottenChildren.clear();
}
List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element>? forgottenChildren, List<Object?>? slots }) {
Element? replaceWithNullIfForgotten(Element child) {
return forgottenChildren != null && forgottenChildren.contains(child) ? null : child;
}
//头部索引
int newChildrenTop = 0;
int oldChildrenTop = 0;
//尾部索引
int newChildrenBottom = newWidgets.length - 1;
int oldChildrenBottom = oldChildren.length - 1;
//创建一个存储新列表的集合
final List<Element> newChildren = List<Element>.filled(newWidgets.length, _NullElement.instance);
Element? previousChild;
return newChildren;
}
首先我们简单梳理一下,这里开始element的更新,主要分为三个阶段进行更新,更新前会先创建索引,通过索引获取对应的widget
- 这里会先定义一个过滤带有globalKey的方法replaceWithNullIfForgotten,后面会用到
- 创建两个头部索引以及尾部索引,方便后期进行遍历更新
遍历更新会分为三个阶段
第一阶段
//第一阶段先对头部索引进行遍历
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
//先清除带有globalkey的索引,因为globalkey不在更新范围内
final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
final Widget newWidget = newWidgets[newChildrenTop];
//只对非活跃状态的element进行更新
assert(oldChild == null || oldChild._lifecycleState == _ElementLifecycle.active);
//直到遇到不可更新的element就退出循环
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) {
break;
}
//如果可更新则进行更新操作
final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;
assert(newChild._lifecycleState == _ElementLifecycle.active);
//将更新后的element进行保存
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
//继续下一次的遍历操作
newChildrenTop += 1;
oldChildrenTop += 1;
}
// 同步扫描尾部节点
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
//与头部索引扫描的操作一致
final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenBottom]);
final Widget newWidget = newWidgets[newChildrenBottom];
assert(oldChild == null || oldChild._lifecycleState == _ElementLifecycle.active);
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) {
break;
}
oldChildrenBottom -= 1;
newChildrenBottom -= 1;
}
流程梳理
- 开始会先前后对满足Widget.canUpdate(oldChild.widget, newWidget)的widget进行更新,并存入newChildren列表中
- 同时更新索引值继续遍历,直到遇到不可更新的节点停止遍历(即类型不同或key不同)
- 头部索引遍历完继续遍历尾部索引,注意这里尾部索引不做任何更新,只是更新索引值,即记录不满足更新条件Widget.canUpdate的索引值。
第二阶段
final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
Map<Key, Element>? oldKeyedChildren;
if (haveOldChildren) {
oldKeyedChildren = <Key, Element>{};
while (oldChildrenTop <= oldChildrenBottom) {
//此时先过滤带有globalkey的element
final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
assert(oldChild == null || oldChild._lifecycleState == _ElementLifecycle.active);
if (oldChild != null) {
if (oldChild.widget.key != null) {
//保存带有key的element
oldKeyedChildren[oldChild.widget.key!] = oldChild;
} else {
//将没有key的element卸载
deactivateChild(oldChild);
}
}
//递增索引值继续遍历
oldChildrenTop += 1;
}
}
//开始遍历剩余节点
while (newChildrenTop <= newChildrenBottom) {
Element? oldChild;
final Widget newWidget = newWidgets[newChildrenTop];
if (haveOldChildren) {
final Key? key = newWidget.key;
if (key != null) {
//通过新widget的key从oldKeyedChildren中获取对应的element
oldChild = oldKeyedChildren![key];
if (oldChild != null) {
//如果有element则判断类型是否相同
if (Widget.canUpdate(oldChild.widget, newWidget)) {
//移除相关的element
oldKeyedChildren.remove(key);
} else {
oldChild = null;
}
}
}
}
assert(oldChild == null || Widget.canUpdate(oldChild.widget, newWidget));
//如果有相同的key则进行更新,计算新的element节点
final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;
//将创建的新的element节点存入newChildren列表
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
}
流程梳理
- 第一步通过oldKeyedChildren记录原来旧的带有key(非globalkey)的element节点以备后续使用
- 第二步开始遍历剩余节点,判断原来的节点与新的widget的节点是否相同。如果相同则晴空万里oldKeyedChildren对应的element节点,基于新的widget进行更新即可,复用原来的element。并将element存入newChildren中
第三阶段
//重新获取旧element和新widget的尾部索引,即重置尾部索引
newChildrenBottom = newWidgets.length - 1;
oldChildrenBottom = oldChildren.length - 1;
//从尾部开始更新,开始更新newChildren的尾部
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final Element oldChild = oldChildren[oldChildrenTop];
assert(replaceWithNullIfForgotten(oldChild) != null);
assert(oldChild._lifecycleState == _ElementLifecycle.active);
final Widget newWidget = newWidgets[newChildrenTop];
assert(Widget.canUpdate(oldChild.widget, newWidget));
final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;
assert(newChild._lifecycleState == _ElementLifecycle.active);
assert(oldChild == newChild || oldChild._lifecycleState != _ElementLifecycle.active);
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}
//开始清空未命中使用的key元素
if (haveOldChildren && oldKeyedChildren!.isNotEmpty) {
for (final Element oldChild in oldKeyedChildren.values) {
if (forgottenChildren == null || !forgottenChildren.contains(oldChild)) {
deactivateChild(oldChild);
}
}
}
流程梳理
- 从第一阶段和第二阶段遍历完之后,开始遍历剩余节点,则重新计算尾部索引,即重置索引开始第三阶段的遍历
- 从尾部开始更新,更新newChildren的尾部。,更新完之后开始清空未命中使用的key元素,并调用deactivateChild清空