一、概述
在 Flutter渲染机制(一):Widget 树 一文中,我们介绍了 Widget 树,发现每一种类型的 Widget 都会创建与之相对应的 Element,那这个 Element 又是什么,有什么作用呢?
Element 是什么:
- Element 是 Widget 的实际内容,并且会根据 Widget 的位置,配置自己在书中的位置。
- Element 会形成一棵树,有的 Element 有一个子节点,有的有多个子节点。
二、Element 的分类
Element 的继承关系图:
结合上图,从功能上看,Element 可以分为 ComponentElement、RenderObjectElement两大类。SingleChildWidgetElementMixin 是做混合时使用的。
-
ComponentElement
- 组合类 Element,相当于容器,主要用来组装其它 Element,得到功能更加复杂的Element。
- ComponentElement 包括
StatelessElement、StatefulElement、ProxyElement
。- StatelessElement对应 StatelessWidget
- StatefulElement对应 StatefulWidget
- ProxyElement 对应 ProxyWidget
-
RenderObjectElement
- 渲染类Element,对应Renderer Widget,是框架最核心的Element。
- RenderObjectElement 主要包括
LeafRenderObjectElement
、SingleChildRenderObjectElement
和MultiChildRenderObjectElement
。- LeafRenderObjectElement 对应 LeafRenderObjectWidget,没有子节点。
- SingleChildRenderObjectElement 对应 SingleChildRenderObjectWidget,有一个子节点。
- MultiChildRenderObjectElement 对应 MultiChildRenderObjecWidget,有多个子节点。
三、ComponentElement
3.1 Widget、Element、State 之间的关系
小结:
- 上图展示了 StatelessElement 与 StatefulElement 在 Element 树中与对应元素的关系。
- 在 Element 树中,StatelessElement 与 StatefulElement 都是通过 parent 和 child 属性持有它的父/子 Element。
- 左图显示了 StatelessElement 与 Widget 的引用关系。即 StatelessElement 持有 Widget 引用。
- 右图显示了 StatefulElement 与 Widget 和 State 的引用关系。即 StatefulElement 持有 Widget 和 State 的引用;同时 State 持有 StatefulElement 和 Widget 的引用。
3.2 核心操作流程
核心操作流程包括:Element 的创建、更新、销毁,下面将分别分析这三个流程。
3.2.1 创建流程
ComponentElement 的创建起点从 父Element 调用 inflateWidget() 方法开始,然后通过mount() 方法将该Element挂载至Element Tree,并递归创建子节点。
- 绿色流程:Child 为 StatelessWidget 类型时的创建流程。
- 红色流程:Child 为 StatefulWidget 类型时的创建流程的差异部分。StatefulElement 重写了
_firstBuild()、performRebuild()
等方法,去执行了 State 相关的操作。
3.2.2 更新流程
父Element 调用 updateChild() 方法执行更新子节点的操作,由于新旧Widget的类型和Key均未发生变化,因此触发了Element的更新操作,并通过 performRebuild 将更新操作传递下去。其核心函数updateChild之后会详细介绍。
3.2.3 销毁流程
父Element 或更上级的节点执行更新子节点的操作(updateChild),由于新旧Widget的类型或者Key发生变化,或者新Widget被移除,因此导致该Element被转为未激活状态,并被加入未激活列表,并在下一帧被失效。
3.3 核心方法分析
从上面的三个核心流程中可以看出,他们都触发了 Element.updateChild()
方法,所以我们先来分析一下它的逻辑。
3.3.1 Element.updateChild()
当 Widget 中状态发生变化时,Element 会重新构建状态发生变化的 Widget,因此会得到一棵新的 Widget 树。Element.updateChild() 方法就是用来比较两棵 Widget 树的差异,然后将新Widget树中变化的部分覆盖到老的Widget树中 (其它两棵树会一起改变)。
因为是两个 widget 的比较,且它们都存在 “为Null” 和 “不为Null” 两种状态,所以共有4种组合方式。如下表所示:
child 状态 | newWidget == null | newWidget != null |
---|---|---|
Child == null | 返回null | 返回新Element |
Child != null | 移除旧的子Element,返回null | 1. 如果Widget能更新,更新旧的子Element,并返回旧的Element。 2. 否则创建新的子Element并返回。 |
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
// 1.Condition2 & Condition3两个场景.
// 即newWidget==null时,不管child!是否有值,都直接返回null。
if (newWidget == null) {
// 如果child有值,则移除child这个Element。
if (child != null)
deactivateChild(child);
return null;
}
final Element newChild;
if (child != null) {
bool hasSameSuperclass = true;
// 2.Condition5的场景:同一个widget对象。
if (hasSameSuperclass && child.widget == newWidget) {
if (child.slot != newSlot) {
// 同一个widget,newSlot位置可能发生变化,所以更新一下Element中的_slot值。
updateSlotForChild(child, newSlot);
}
newChild = child;
// 3.Condition4的场景
// widget是新建的,但是widget的runtimeType和key相同。
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot) {
updateSlotForChild(child, newSlot);
}
// 因为widget是新建的,所以需要
child.update(newWidget);
newChild = child;
} else {
// 4.Condition5的场景:
// newWidget与child.widget的runtimeType或key不相同,所以要先移除child(Element),然后新建newWidget对应的Element。
deactivateChild(child);
newChild = inflateWidget(newWidget, newSlot);
}
} else {
// 5.Condition1场景:
// 即child==null && newWidget==null成立,相当于新增widget。
newChild = inflateWidget(newWidget, newSlot);
}
return newChild;
}
/**
* Widget.class
* 作用:比较两个widget的runtimeType和key是否相同。
*/
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
上面代码中展示了5种Condition的触发场景。
小结:
- 当 newWidget == null 时,说明是移除当前位置的Element。
- Condition1:此时如果 child(Element) == null,直接返回 null。
- Condition2:此时如果 child != null,说明之前这个位置有Element,则先移除 Element,再返回 null。- 当 newWidget != null 时,说明是在新增/修改当前位置Element。
- Condition3:此时如果 child(Element) == null,说明是新增Element的场景,因此直接使用newWidget构建Element。
- 此时如果 child != null,说明之前这个位置有Element,这是又分为两种情况。
- Condition4:当Element可以复用时(即newWidget的runtimeType和key相同与child.widget相同时),所以直接更新旧的 Element 即可。
- Condition5:当Element不可以复用时,使用newWidget重新构建 Element 并返回。
3.3.2 Element.inflateWidget()
Element inflateWidget(Widget newWidget, Object? newSlot) {
final Key? key = newWidget.key;
// 1.复用Element的逻辑。
if (key is GlobalKey) {
// 1.1 查找复用Element.
final Element? newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
// 1.2 将复用的Element关联到Element树上,即与parent建立关系。
newChild._activateWithParent(this, newSlot);
// 1.3 因为是复用的,所以只需要执行更新逻辑即可。
final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
return updatedChild!;
}
}
// 2.创建 newWidget 的 Element。
final Element newChild = newWidget.createElement();
// 3. 触发Element.mount()方法,执行构建逻辑。
newChild.mount(this, newSlot);
return newChild;
}
Element? _retakeInactiveElement(GlobalKey key, Widget newWidget) {
// 1.先从BuildOwner中查找对应的GlobalKey.
final Element? element = key._currentElement;
if (element == null)
return null;
// 2.查找GlobalKey相同的Element后,需要对比两个widget的runtimeType或key是否相同.
if (!Widget.canUpdate(element.widget, newWidget))
return null;
// 3.到这里说明已经找到了可以复用的Element。
// 因为复用的Element是在Element树上的,所以要断开它原先的父/子节点。相当于双链表删除一个Node节点的操作。
// 3.1 查找当前节点的父节点。
final Element? parent = element._parent;
if (parent != null) {
// 3.2 父节点断开对当前节点的引用信息。
parent.forgetChild(element);
// 3.3 子节点断开与父节点的引用关系。
parent.deactivateChild(element);
}
// 4.Element被复用了,所以从_inactiveElements移除。
owner!._inactiveElements.remove(element);
return element;
}
小结: Element.inflateWidget() 方法执行 Element 的构建,在构建时会尝试复用之前的 Element (优化的手段),避免重复创建 ELement。
- 判断 newWidget 是否有 GlobalKey,如果有GlobalKey,则尝试从 InactiveElements 列表中查找 Element 并进行复用。
- 先从BuildOwner中查找对应的GlobalKey。
- 查到GlobalKey相同的Element后,对比两个widget的runtimeType或key是否相同。
- 到这里已经可以拿到复用的Element了,因此将复用的Element从Element树上卸载再来。
- 将拿到的Element从BuildOwner._inactiveElements 中移除。
- 执行完上面步骤后,Element 就从 Element 树上剥离了,此时需要将它挂载到新的 parent 节点下。
- 执行 Element 的更新逻辑。
- 无可复用Element,则根据 newWidget 创建对应的 Element,并将其挂载至 Element 树。
3.3.3 Element.mount()
// Element.class
abstract class Element extends DiagnosticableTree implements BuildContext {
void mount(Element? parent, Object? newSlot) {
_parent = parent;
_slot = newSlot;
// 设置当前Element的状态为active。
_lifecycleState = _ElementLifecycle.active;
// Element元素的层级。
_depth = _parent != null ? _parent!.depth + 1 : 1;
if (parent != null) { //传递BuildOwner
_owner = parent.owner;
}
final Key? key = widget.key;
if (key is GlobalKey) {
// 这里就是Element.inflateWidget()方法中复用Element的逻辑。
owner!._registerGlobalKey(key, this);
}
_updateInheritance();
}
void rebuild() {
// 通过判断当前Element状态以及_dirty的状态,来减少不必要的绘制。
if (_lifecycleState != _ElementLifecycle.active || !_dirty)
return;
performRebuild();
}
}
// ComponentElement.class
abstract class ComponentElement extends Element {
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
// Element挂载到Element树上的时候,会触发首次的构建流程。
_firstBuild();
}
void _firstBuild() {
rebuild(); // Element.rebuild()
}
}
小结:
当 Element 第一次被插入Element 树时,会调用Element.mount()方法。其功能如下:
- 将给 Element 挂载到 Element 树上,然后更新_parent,_slot,层级等树相关的属性。
- 如果新Widget有GlobalKey,将该 Element 注册进 GlobalKey 中,在Element.inflateWidget()方法的复用Element的地方会被使用。
- Element 的子类会重载 mount() 方法,如在
ComponentElement.mount()
方法会调用_firstBuild函数,触发子Widget的创建和更新。
3.3.4 ComponentElement.performRebuild()
void performRebuild() {
Widget? built;
try {
// 1.触发ComponentElement.build()逻辑,生成子Widget。
// 这里会触发StatelessWidget.build() 或 State.build()的调用。
built = build();
} catch (e, stack) {
built = ErrorWidget.builder(...);
} finally {
_dirty = false;
}
try {
// 2.根据新的子Widget更新子Element
_child = updateChild(_child, built, slot);
} catch (e, stack) {
built = ErrorWidget.builder(...);
_child = updateChild(null, built, slot);
}
}
小结:
ComponentElement.performRebuild() 会做两件事。
- 构建子 Widget。
- 根据新的子Widget 更新子Element。
3.3.5 Element.update()
在ComponentElement.performRebuild()中,我们看到最终又调用 Element.updateChild() 方法进行子节点的创建, Element.updateChild() 方法我们已经分析过了新建子节点的过程,下面我们来分析下子节点的更新逻辑。
Element.updateChild() 方法中 Condition4 即为更新子节点的逻辑,他会调用 Element.update() 方法来执行更新逻辑。
// Element.class
void update(covariant Widget newWidget) {
_widget = newWidget;
}
// StatelessElement.class
void update(StatelessWidget newWidget) {
super.update(newWidget);
// 1._dirty标记为true,在Element.rebuild()方法中会对这个标识进行判断。
_dirty = true;
// 2.执行构建逻辑。
rebuild();
}
// StatefulElement.class
void update(StatefulWidget newWidget) {
super.update(newWidget);
final StatefulWidget oldWidget = state._widget!;
// 1._dirty标记为true,在Element.rebuild()方法中会对这个标识进行判断。
_dirty = true;
state._widget = widget as StatefulWidget;
// 2.触发State.didUpdateWidget()进行更新。
final Object? debugCheckForReturnedFuture = state.didUpdateWidget(oldWidget) as dynamic;
// 3.执行构建逻辑。
rebuild();
}
小结:
- 不同的 Element 会重写 update() 方法,执行特定的逻辑。
- 基类中的 Element.update() 方法主要是将对应的Widget更新为新的Widget。
- 在 StatelessElement 中,会将当前 Element 标识为 dirty,并调用 rebuild 方法触发对子Widget的重建。
- 在 StatefulElement 中,会将当前 Element 标识为 dirty,然后调用 State.didUpdateWidget() 方法执行对应的更新逻辑, 最后调用 rebuild 方法触发对子Widget的重建。
3.3.6 Element 的销毁流程
Element 的销毁流程主要分为两步:
- Step1:标记Element状态。将要销毁的 Element 从 Element树中移除,并将要销毁的 Element 及其子Element 的状态改为 inactive。
- Step2:卸载Element。当触发下一帧的渲染回调时,会调用到 BuildOwner.finalizeTree() 方法,最终会调用到 Element.unmount() 方法。
Step1 的调用流程如下:
Element.deactivateChild()
-> BuildOwner._inactiveElements.add(Element) (即:_InactiveElements.add())
-> _InactiveElements._deactivateRecursively():
-> Element.deactivate()
Step2 的调用流程如下:
RendererBinding.initInstances()
-> RendererBinding._handlePersistentFrameCallback()
-> Widget.drawFrame()
-> buildOwner!.finalizeTree()
-> _InactiveElements._unmountAll()
-> _InactiveElements._unmount()
-> Element.unmount()
Step1的流程:
void deactivateChild(Element child) {
// 断开child与parent的关联,即从Element树中移除。
child._parent = null;
// 触发RenderObject组件对应的声明周期。
child.detachRenderObject();
// 放入未激活状态的Element回收池。
owner!._inactiveElements.add(child); // this eventually calls child.deactivate()
}
// _InactiveElements.class
class _InactiveElements {
void add(Element element) {
if (element._lifecycleState == _ElementLifecycle.active)
// 当前Element状态如果为active,需要将它及其子节点的状态都变成inactive。
_deactivateRecursively(element);
// 将传入的Element放入_elements中,当下一帧绘制开始时,会触发BuildOwner.finalizeTree()方法,在这个方法中通过递归遍历的方式将_elements中的Element及其子节点全部卸载。
_elements.add(element);
}
/**
* 递归遍历,将传入的Element及其所有子Element状态都变成inactive。
* 遍历方式:树的先序遍历。
*/
static void _deactivateRecursively(Element element) {
element.deactivate(); //将 Element 状态标记为inactive
element.visitChildren(_deactivateRecursively);
}
}
/**
* Element.class
* 将 Element 状态标记为inactive
*/
void deactivate() {
if (_dependencies != null && _dependencies!.isNotEmpty) {
for (final InheritedElement dependency in _dependencies!)
dependency._dependents.remove(this);
_inheritedWidgets = null;
_lifecycleState = _ElementLifecycle.inactive;
}
小结:
Step1的执行流程:将要销毁的 Element 从 Element树中移除,并将要销毁的 Element 及其子Element 的状态改为 inactive。
Step2的流程:
// BuildOwner.class
void finalizeTree() {
lockState(() {
// 在Element.deactivateChild()中,会将移除的Element放入_inactiveElements。
_inactiveElements._unmountAll(); // this unregisters the GlobalKeys
});
}
class _InactiveElements {
void _unmountAll() {
_locked = true;
final List<Element> elements = _elements.toList()..sort(Element._sort);
_elements.clear();
try {
elements.reversed.forEach(_unmount); //_unmount是个方法。
} finally {
_locked = false;
}
}
/**
* 卸载传入Element下的所有子Element。
* 通过递归方式进行遍历,先卸载叶子节点,在卸载父节点。
*/
void _unmount(Element element) {
// 访问当前Element的所有子Element。
element.visitChildren((Element child) {
_unmount(child);
});
// 调用 Element.unmount()方法进行资源释放。
element.unmount();
}
}
小结:
Step2的执行流程:卸载Element。当触发下一帧的渲染回调时,会调用到 BuildOwner.finalizeTree() 方法,最终会调用到 Element.unmount() 方法。
到这里 Element 的创建流程、更新流程和销毁流程就已经介绍完了。
四、RenderObjectElement
4.1 Widget、Element、RenderObject 之间的关系
小结:
- RenderObjectElement 持有 Parent Element,但是不一定持有Child Element,有可能无Child Element,有可能持有一个Child Element(Child),有可能持有多个Child Element(Children)。
- RenderObjectElement 持有对应的 Widget 和 RenderObject,将Widget、RenderObject串联起来,实现了 Widget、Element、RenderObject 之间的绑定。
4.2 核心操作流程
与 ComponentElement 一样,RenderObjectElement 的核心操作流程包括:Element 的创建、更新、销毁,下面将分别分析这三个流程。
4.2.1 创建流程
RenderObjectElement 的创建流程和 ComponentElement 的创建流程基本一致,其最大的区别是ComponentElement在mount后,会调用 build 创建子Widget,而RenderObjectElement 则是 create 和 attach 其 RenderObject。
4.2.2 更新流程
RenderObjectElement 的更新流程和 ComponentElement 的更新流程也基本一致,区别在于:
- ComponentElement 的 update() 方法会调用 build() 方法,重新触发子Widget的构建。
- RenderObjectElement 则是调用 updateRenderObject 对绑定的 RenderObject 进行更新。
4.2.3 销毁流程
RenderObjectElement 的销毁流程和 ComponentElement 的销毁流程也基本一致。都是由父Element或更上级的节点执行更新子节点的操作(updateChild),导致该Element被停用,并被加入未激活列表,在下一帧触发回调时进行卸载。RenderObjectElement 的销毁操作与 StatefulElement 比较相似:
- RenderObjectElement 调用 unmount() 方法时,会调用 Widget.didUnmountRenderObject() 方法进行资源的销毁。
- StatefulElement 调用 unmount() 方法时,会调用 State.dispose() 方法进行资源的销毁。
4.3 核心方法分析
4.3.1 Element.inflateWidget()
该方法属于Element,所以RenderObjectElement子类都没有重写。在 ComponentElement 部分已经分析过了,此处不在赘述。
4.3.2 Element.mount()
// RenderObjectElement.class
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
// 1.Widget触发RenderObject的创建。
_renderObject = widget.createRenderObject(this);
// 2.将当前的RenderObject关联到RenderObject树中。
attachRenderObject(newSlot);
_dirty = false;
}
// SingleChildRenderObjectElement.class
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
// 调用 Element.updateChild()方法。
_child = updateChild(_child, widget.child, null);
}
// MultiChildRenderObjectElement.class
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
// 我们在Widget中设置的children属性。
final List<Element> children = List<Element>.filled(widget.children.length, _NullElement.instance, growable: false);
Element? previousChild;
for (int i = 0; i < children.length; i += 1) {
// 遍历生成所有的子Widget.
final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element?>(i, previousChild));
children[i] = newChild;
previousChild = newChild;
}
_children = children;
}
小结:
- RenderObjectElement.mount() 方法中调用 createRenderObject 创建 RenderObject,并使用 attachRenderObject 将 RenderObject 通过Element树关联到 RenderObject 树上。
- SingleChildRenderObjectElement 调用 updateChild() 方法创建/更新子节点。
- MultiChildRenderObjectElement 调用每个子节点的 inflateWidget 重建所有子Widget和子Element。
4.3.3 RenderObjectElement.performRebuild()
void performRebuild() {
// 触发Widget对应的声明周期函数。
// 即在updateRenderObject方法中更新对应的RenderObject。
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
小结:
- 调用 Widget.updateRenderObject() 方法更新对应的RenderObject。
4.3.4 RenderObjectElement.update()
// RenderObjectElement.class
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
// 触发Widget对应的声明周期函数。
// 即在updateRenderObject方法中更新对应的RenderObject。
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
// SingleChildRenderObjectElement.class
void update(SingleChildRenderObjectWidget newWidget) {
super.update(newWidget);
// 调用Element.updateChild()触发更新逻辑。该方法在ComponentElement部分已经分析过。
_child = updateChild(_child, widget.child, null);
}
// MultiChildRenderObjectElement.class
void update(MultiChildRenderObjectWidget newWidget) {
super.update(newWidget);
// 更新子节点,这部分比较复杂,后面单独分析。
_children = updateChildren(_children, widget.children, forgottenChildren: _forgottenChildren);
_forgottenChildren.clear();
}
小结:
- RenderObjectElement.update() 方法将对应的 Widget 更新为新的 Widget。
- RenderObjectElement.update() 方法内调用 updateRenderObject 更新对应的RenderObject。
4.3.5 RenderObjectElement.updateChildren()
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;
}
Object? slotFor(int newChildIndex, Element? previousChild) {
return slots != null
? slots[newChildIndex]
: IndexedSlot<Element?>(newChildIndex, previousChild);
}
// ------------ START --------------
int newChildrenTop = 0;
int oldChildrenTop = 0;
int newChildrenBottom = newWidgets.length - 1;
int oldChildrenBottom = oldChildren.length - 1;
final List<Element> newChildren = oldChildren.length == newWidgets.length ?
oldChildren : List<Element>.filled(newWidgets.length, _NullElement.instance, growable: false);
Element? previousChild;
// 1.从上到下开始遍历新旧两棵树,比较同一个位置的Widget是否可以复用。
// - 如果可以复用Widget,就复用Widget进行更新,然后比较下一个位置。
// - 如果不可以复用,则退出循环。
// 复用的标准是Widget的runtimeType和key都相同
// Update the top of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
final Widget newWidget = newWidgets[newChildrenTop];
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}
// 2.从下到上开始遍历新旧两棵树,比较同一个位置的Widget是否可以复用。
// - 如果可以复用Widget,就复用Widget进行更新,然后比较下上个位置。
// - 如果不可以复用,则退出循环。
// Scan the bottom of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenBottom]);
final Widget newWidget = newWidgets[newChildrenBottom];
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
oldChildrenBottom -= 1;
newChildrenBottom -= 1;
}
// 3.尝试将新旧两棵树不一样的Element进行回收,等待复用。
// haveOldChildren为true表示新旧两棵树存在差异,需要进行额外处理。
// Scan the old children in the middle of the list.
final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
// Element复用的缓存池。
Map<Key, Element>? oldKeyedChildren;
if (haveOldChildren) {
oldKeyedChildren = <Key, Element>{};
while (oldChildrenTop <= oldChildrenBottom) {
final Element? oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
if (oldChild != null) {
// 存放有key的的Element.
if (oldChild.widget.key != null)
oldKeyedChildren[oldChild.widget.key!] = oldChild;
else
deactivateChild(oldChild); //释放掉没有设置Key的Element
}
oldChildrenTop += 1;
}
}
// 4.更新新旧两棵树差异的部分。
// 4.1 先尝试从Element复用池oldKeyedChildren中获取,满足条件了就直接复用。
// 4.2 不满足复用条件,则直接新建ELement。
// Update the middle of the list.
while (newChildrenTop <= newChildrenBottom) {
Element? oldChild;
final Widget newWidget = newWidgets[newChildrenTop];
if (haveOldChildren) {
final Key? key = newWidget.key;
if (key != null) {
oldChild = oldKeyedChildren![key];
if (oldChild != null) {
if (Widget.canUpdate(oldChild.widget, newWidget)) {
// we found a match!
// remove it from oldKeyedChildren so we don't unsync it later
oldKeyedChildren.remove(key);
} else {
// Not a match, let's pretend we didn't see it for now.
oldChild = null;
}
}
}
}
final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
}
// 5.复位newChildrenBottom和oldChildrenBottom指向的位置。
// We've scanned the whole list.
newChildrenBottom = newWidgets.length - 1;
oldChildrenBottom = oldChildren.length - 1;
// 6.遍历:从第二步的从下往上更新停止的位置开始,到最后一个位置。
// 为什么第二步更新过之后,这里还需要更新呢?
// 因为在新旧两棵树差异化的部分,可能存在ELement所在的slot位置发生变化,所以需要重新执行更新的逻辑。
// Update the bottom of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final Element oldChild = oldChildren[oldChildrenTop];
final Widget newWidget = newWidgets[newChildrenTop];
final Element newChild = updateChild(oldChild, newWidget, slotFor(newChildrenTop, previousChild))!;
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}
// 7.oldKeyedChildren中存在部分没有复用的元素,需要对这部分元素进行回收。
// Clean up any of the remaining middle nodes from the old list.
if (haveOldChildren && oldKeyedChildren!.isNotEmpty) {
for (final Element oldChild in oldKeyedChildren.values) {
if (forgottenChildren == null || !forgottenChildren.contains(oldChild))
deactivateChild(oldChild);
}
}
return newChildren;
}
小结:
RenderObjectElement.updateChildren() 方法的主要职责:
- 复用能复用的子节点,并调用updateChild对子节点进行更新。
- 对不能更新的子节点,调用deactivateChild对该子节点进行失效。
步骤如下:
- 从顶部向下更新子Element。
- 从底部向上扫描子Element。
- 扫描旧的子Element列表里面中间的子Element,保存Widget有Key的Element到oldKeyChildren,其他的失效。
- 对于新的子Element列表,如果其对应的Widget的Key和oldKeyChildren中的Key相同,更新oldKeyChildren中的Element。
- 从下到上更新底部的Element。
- 清除旧子Element列表中其他所有剩余Element。
动图效果可以参考:Flutter 必知必会系列—— Element 的更新复用机制
五、小结
本文主要介绍了Element相关知识:
- 介绍了 Element 的分类 (
ComponentElement、RenderObjectElement
),及其不同类型的特点。
2. ComponentElement 负责组合子Element。
3. RenderObjectElement 后者负责渲染。 - 介绍了 Element 的生命周期,以及核心流程 (
创建、更新、销毁
)。 - Element 树的三棵树的核心,通过 Element 树可以构建 Widget 树和 RenderObject 树。
- Element 的主要复用和更新逻辑由其核心方法
updateChild
实现,具体逻辑见上文。
Element树与Widget树关联关系: