Flutter框架分析(三)-- Element(flutter架构设计)
ccwgpt 2024-09-15 14:56 26 浏览 0 评论
1. 前言
上一篇文章讲到,Widget是描述一个UI元素的配置数据,Element才真正代表屏幕显示元素,是某个位置的Widget生成的实例。本篇文章则主要介绍Element的主要功能。
通过上篇文章介绍的Widget Tree,Flutter Framework会生成一系列Element,这些Element构成了Element Tree,其主要功能如下:
- 维护这棵Element Tree,根据Widget Tree的变化来更新Element Tree,包括:节点的插入、更新、删除、移动等;
- 将Widget和RenderObject关联到Element Tree上。
2. Element分类
如上图所示,Element从功能上看,可以分为两大类:
- ComponentElement
组合类Element。这类Element主要用来组合其他更基础的Element,得到功能更加复杂的Element。开发时常用到的StatelessWidget和StatefulWidget相对应的Element:StatelessElement和StatefulElement,即属于ComponentElement。
- RenderObjectElement
渲染类Element,对应Renderer Widget,是框架最核心的Element。RenderObjectElement主要包括LeafRenderObjectElement,SingleChildRenderObjectElement,和MultiChildRenderObjectElement。其中,LeafRenderObjectElement对应的Widget是LeafRenderObjectWidget,没有子节点;SingleChildRenderObjectElement对应的Widget是SingleChildRenderObjectWidget,有一个子节点;MultiChildRenderObjectElement对应的Widget是MultiChildRenderObjecWidget,有多个子节点。
3. Element生命周期
Element有4种状态:initial,active,inactive,defunct。其对应的意义如下:
- initial:初始状态,Element刚创建时就是该状态。
- active:激活状态。此时Element的Parent已经通过mount将该Element插入Element Tree的指定的插槽处(Slot),Element此时随时可能显示在屏幕上。
- inactive:未激活状态。当Widget Tree发生变化,Element对应的Widget发生变化,同时由于新旧Widget的Key或者的RunTimeType不匹配等原因导致该Element也被移除,因此该Element的状态变为未激活状态,被从屏幕上移除。并将该Element从Element Tree中移除,如果该Element有对应的RenderObject,还会将对应的RenderObject从Render Tree移除。但是,此Element还是有被复用的机会,例如通过GlobalKey进行复用。
- defunct:失效状态。如果一个处于未激活状态的Element在当前帧动画结束时还是未被复用,此时会调用该Element的unmount函数,将Element的状态改为defunct,并对其中的资源进行清理。
Element4种状态间的转换关系如下图所示:
4. ComponentElement
4.1 与核心元素关系
如上文所述,ComponentElement分为StatelessElement和StatefulElement,这两种Element同核心元素Widget以及State之间的关系如下图所示。
如图:
- ComponentElement持有Parent Element及Child Element,由此构成Element Tree.
- ComponentElement持有其对应的Widget,对于StatefulElement,其还持有对应的State,以此实现Element和Widget之间的绑定。
- State是被StatefulElement持有,而不是被StatefulWidget持有,便于State的复用。事实上,State和StatefulElement是一一对应的,只有在初始化StatefulElement时,才会初始化对应的State并将其绑定到StatefulElement上。
4.2 核心流程
一个Element的核心操作流程有,创建、更新、销毁三种,下面将分别介绍这三个流程。
- 创建
ComponentElement的创建起源与父Widget调用inflateWidget,然后通过mount将该Element挂载至Element Tree,并递归创建子节点。
- 更新
由父Element执行更新子节点的操作(updateChild),由于新旧Widget的类型和Key均未发生变化,因此触发了Element的更新操作,并通过performRebuild将更新操作传递下去。其核心函数updateChild之后会详细介绍。
- 销毁
由父Element或更上级的节点执行更新子节点的操作(updateChild),由于新旧Widget的类型或者Key发生变化,或者新Widget被移除,因此导致该Element被转为未激活状态,并被加入未激活列表,并在下一帧被失效。
4.3 核心函数
下面对ComponentElement中的核心方法进行介绍。
- inflateWidget
Element inflateWidget(Widget newWidget, dynamic newSlot) {
final Key key = newWidget.key;
//复用GlobalKey对应的Element
if (key is GlobalKey) {
final Element newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
newChild._activateWithParent(this, newSlot);
final Element updatedChild = updateChild(newChild, newWidget, newSlot);
return updatedChild;
}
}
//创建Element,并挂载至Element Tree
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
return newChild;
}
inflateWidget的主要职责如下:
- 判断新Widget是否有GlobalKey,如果有GlobalKey,则从Inactive Elements列表中找到对应的Element并进行复用。
- 无可复用Element,则根据新Widget创建对应的Element,并将其挂载至Element Tree。
- mount
void mount(Element parent, dynamic newSlot) {
//更新_parent等属性,将元素加入Element Tree
_parent = parent;
_slot = newSlot;
_depth = _parent != null ? _parent.depth + 1 : 1;
_active = true;
if (parent != null) // Only assign ownership if the parent is non-null
_owner = parent.owner;
//注册GlobalKey
final Key key = widget.key;
if (key is GlobalKey) {
key._register(this);
}
_updateInheritance();
}
当Element第一次被插入Element Tree的时候,该方法被调用。其主要职责如下:
- 将给Element加入Element Tree,更新_parent,_slot等树相关的属性。
- 如果新Widget有GlobalKey,将该Element注册进GlobalKey中,其作用下文会详细分析。
- ComponentElement的mount函数会调用_firstBuild函数,触发子Widget的创建和更新。
- performRebuild
@override
void performRebuild() {
//调用build函数,生成子Widget
Widget built;
built = build();
//根据新的子Widget更新子Element
_child = updateChild(_child, built, slot);
}
performRebuild的主要职责如下:
- 调用build函数,生成子Widget。
- 根据新的子Widget更新子Element。
- update
@mustCallSuper
void update(covariant Widget newWidget) {
_widget = newWidget;
}
此函数主要职责为:
- 将对应的Widget更新为新的Widget。
- 在ComponentElement的各种子类中,还会调用rebuild函数触发对子Widget的重建。
- updateChild
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {
//新的Child Widget为null,则返回null;如果旧Child Widget,使其未激活
if (child != null)
deactivateChild(child);
return null;
}
Element newChild;
if (child != null) {
//新的Child Widget不为null,旧的Child Widget也不为null
bool hasSameSuperclass = true;
if (hasSameSuperclass && child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)){
//Key和RuntimeType相同,使用update更新
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
newChild = child;
} else {
//Key或RuntimeType不相同,使旧的Child Widget未激活,并对新的Child Widget使用inflateWidget
deactivateChild(child);
newChild = inflateWidget(newWidget, newSlot);
}
} else {
//新的Child Widget不为null,旧的Child Widget为null,对新的Child Widget使用inflateWidget
newChild = inflateWidget(newWidget, newSlot);
}
return newChild;
}
该方法的主要职责为:
根据新的子Widget,更新旧的子Element,或者得到新的子Element。其核心逻辑可以用表格表示:
newWidget == null | newWidget != null | |
Child == null | 返回null | 返回新Element |
Child != null | 移除旧的子Element,返回null | 如果Widget能更新, 更新旧的子Element,并返回之;否则创建新的子Element并返回。 |
该逻辑概括如下:
- 如果newWidget为null,则返回null,同时如果有旧的子Element则移除之。
- 如果newWidget不为null,旧Child为null,则创建新的子Element,并返回之。
- 如果newWidget不为null,旧Child不为null,新旧子Widget的Key和RuntimeType等都相同,则调用update方法更新子Element并返回之。
- 如果newWidget不为null,旧Child不为null,新旧子Widget的Key和RuntimeType等不完全相同,则说明Widget Tree有变动,此时移除旧的子Element,并创建新的子Element,并返回之。
5. RenderObjectElement
5.1 与核心元素关系
RenderObjectElement同核心元素Widget及RenderObject之间的关系如下图所示:
如图:
- RenderObjectElement持有Parent Element,但是不一定持有Child Element,有可能无Child Element,有可能持有一个Child Element(Child),有可能持有多个Child Element(Children)。
- RenderObjectElement持有对应的Widget和RenderObject,将Widget、RenderObject串联起来,实现了Widget、Element、RenderObject之间的绑定。
5.2 核心流程
如ComponentElement一样,RenderObjectElement的核心操作流程有,创建、更新、销毁三种,接下来会详细介绍这三种流程。
- 创建
RenderObjectElement的创建流程和ComponentElement的创建流程基本一致,其最大的区别是ComponentElement在mount后,会调用build来创建子Widget,而RenderObjectElement则是create和attach其RenderObject。
- 更新
RenderObjectElement的更新流程和ComponentElement的更新流程也基本一致,其最大的区别是ComponentElement的update函数会调用build函数,重新触发子Widget的构建,而RenderObjectElement则是调用updateRenderObject对绑定的RenderObject进行更新。
- 销毁
RenderObjectElement的销毁流程和ComponentElement的销毁流程也基本一致。也是由父Element或更上级的节点执行更新子节点的操作(updateChild),导致该Element被停用,并被加入未激活列表,并在下一帧被失效。其不一样的地方是在unmount Element的时候,会调用didUnmountRenderObject失效对应的RenderObject。
5.3 核心函数
下面对RenderObjectElement中的核心方法进行介绍。
- inflateWidget
该函数和ComponentElement的inflateWidget函数完全一致,此处不再复述。
- mount
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
_dirty = false;
}
该函数的调用时机和ComponentElement的一致,当Element第一次被插入Element Tree的时候,该方法被调用。其主要职责也和ComponentElement的一致,此处只列举不一样的职责,职责如下:
- 调用createRenderObject创建RenderObject,并使用attachRenderObject将RenderObject关联到Element上。
- SingleChildRenderObjectElement会调用updateChild更新子节点,MultiChildRenderObjectElement会调用每个子节点的inflateWidget重建所有子Widget。
- performRebuild
@override
void performRebuild() {
//更新renderObject
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
performRebuild的主要职责如下:
调用updateRenderObject更新对应的RenderObject。
- update
@override
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
update的主要职责如下:
- 将对应的Widget更新为新的Widget。
2) 调用updateRenderObject更新对应的RenderObject。
- updateChild
该函数和ComponentElement的inflateWidget函数完全一致,此处不再复述。
- updateChildren
@protected
List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element> forgottenChildren }) {
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>(newWidgets.length);
Element previousChild;
// 从顶部向下更新子Element
// 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, IndexedSlot<Element>(newChildrenTop, previousChild));
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}
// 从底部向上扫描子Element
// 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;
}
// 扫描旧的子Element列表里面中间的子Element,保存Widget有Key的Element到oldKeyChildren,其他的失效
// Scan the old children in the middle of the list.
final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
Map<Key, Element> oldKeyedChildren;
if (haveOldChildren) {
oldKeyedChildren = <Key, Element>{};
while (oldChildrenTop <= oldChildrenBottom) {
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
if (oldChild != null) {
if (oldChild.widget.key != null)
oldKeyedChildren[oldChild.widget.key] = oldChild;
else
deactivateChild(oldChild);
}
oldChildrenTop += 1;
}
}
// 根据Widget的Key更新oldKeyChildren中的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, IndexedSlot<Element>(newChildrenTop, previousChild));
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
}
newChildrenBottom = newWidgets.length - 1;
oldChildrenBottom = oldChildren.length - 1;
// 从下到上更新底部的Element。.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final Element oldChild = oldChildren[oldChildrenTop];
final Widget newWidget = newWidgets[newChildrenTop];
final Element newChild = updateChild(oldChild, newWidget, IndexedSlot<Element>(newChildrenTop, previousChild));
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}
// 清除旧子Element列表中其他所有剩余Element
// 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;
}
该函数的主要职责如下:
- 复用能复用的子节点,并调用updateChild对子节点进行更新。
- 对不能更新的子节点,调用deactivateChild对该子节点进行失效。
其步骤如下:
- 从顶部向下更新子Element。
- 从底部向上扫描子Element。
- 扫描旧的子Element列表里面中间的子Element,保存Widget有Key的Element到oldKeyChildren,其他的失效。
- 对于新的子Element列表,如果其对应的Widget的Key和oldKeyChildren中的Key相同,更新oldKeyChildren中的Element。
- 从下到上更新底部的Element。
- 清除旧子Element列表中其他所有剩余Element。
6. 小结
本文主要介绍了Element相关知识,重点介绍了其分类,生命周期,和核心函数。重点如下:
- 维护Element Tree,根据Widget Tree的变化来更新Element Tree,包括:节点的插入、更新、删除、移动等;并起到纽带的作用,将Widget以及RenderObject关联到Element Tree上。
- Element分为ComponentElement和RenderObjectElement,前者负责组合子Element,后者负责渲染。
- Element的主要复用和更新逻辑由其核心函数updateChild实现,具体逻辑见上。
7. 相关文章
相关推荐
- NET版本众多,傻傻分不清楚
-
面对.NET众多的版本,尤其还有几个名称的情况下,相信很多初学C#的开发人员都很困惑,搞不清究竟该怎么选择。下面就列举一下.NET的主要版本及其区别:.NETFramework(传统版本,仅适用于W...
- .NET Framework 和 .NET Core 有啥区别?如何选择?
-
.NETFramework和.NETCore都是由Microsoft开发的软件框架,用于创建Windows应用程序和Web应用程序。它们的主要区别在于:支持的操作系统:.NET...
- 「分享」介绍一款倍受欢迎的.NET 开源UI库
-
概述今天要带大家了解的是一款WPF的开源控件库MahApps.Metro。MahApps.Metro是用于创建现代WPF应用程序的工具包,它许多开箱即用的好东西。目前支持的NETFramework...
- .NET 5.0正式版发布:应用可在ARM64上原生运行
-
更多:o梵蒂冈图书馆频繁遭黑客攻击oNPM包被发现窃取敏感的Discord和浏览器文件o作者:硬核老王o(本文字数:712,阅读时长大约:1分钟).NET5.0正式版发布:应用可...
- 盘点8个热门.Net开源项目
-
一、SmartFormat:轻量级文本模板库,轻松替代string.Format项目地址:https://github.com/axuno/SmartFormatSmartFormat不仅继承了s...
- .NET与Java开发:一场从框架到应用实例的深度对决
-
在软件开发这片广袤的战场上,.NET与Java两大开发平台如同两位身经百战的将军,各自率领着庞大的开发者队伍,在不同的应用场景中大放异彩。今天,我们就来一场从框架到应用实例的深度对决,看看这两大平台究...
- TouchSocket:一个功能强大且易于使用的 .NET 网络通信框架
-
项目介绍TouchSocket是一个功能强大且易于使用的.NET网络通信框架,适用于C#、VB.Net和F#等语言。它提供了多种通信模块,包括TCP、UDP、SSL、WebSocket、Mo...
- 远离报错烦恼!深入全面掌握.NET Framework
-
由于Windows系统对.NETFramework这一系统组件有着极为特殊的要求,而部分应用软件及游戏对其的依赖性也近乎达到了驱动级的水准,使用或安装不当会遭遇许多“未知”的问题,因此如何掌握.NE...
- 想自己搭建.Net Web框架,开源项目太庞大看不懂,可以看这个教程
-
大家好,我是编程乐趣。一直以来,我都在运营知识星球,这个月也开始全职专心编写教程了。当时编写教程,就是发现很多程序员工作多年了,都没自己搭建过框架,也没接触过公司的框架底层代码。这就导致一些问题,无法...
- .NET 8 + React 18 一体化开发框架!苏州码农十年匠心打磨
-
开篇前言从2014年入行至今,从WebForms到MVC,从JavaScript到React/Vue,从.NETFramework到.NETCore/8,技术栈的变迁伴随了我整个职业生涯。去年,我...
- .Net Framework详解
-
相信有不少小伙伴遇到过这种情况:安装软件时提示.NetFramework未安装导致软件无法打开,或者需要安装.NetFramework4.0以上的组件。那.NetFramework是什么呢?....
- 系统小技巧:深入全面掌握.NET Framework
-
由于Windows系统对.NETFramework这一系统组件有着极为特殊的要求,而部分应用软件及游戏对其的依赖性也近乎达到了驱动级的水准,使用或安装不当会遭遇许多“未知”的问题,因此如何掌握.NE...
- 前端架构师成长之路:如何在 Vue 的计算属性中传递参数
-
在Vue中,计算属性(computed)是从其他响应式属性派生的属性,是用于自动监听响应式属性的变化,从而动态计算返回值。计算属性(computed)通常是一个没有参数的函数。当然如果需要像调...
- Vue2 vs Vue3:核心差异与升级指南
-
Vue3自2020年发布以来,凭借其革命性的改进迅速成为开发者关注的焦点。本文将从架构设计、API模式、性能优化等多个维度深入对比Vue2和Vue3的核心差异,并提供代码示例帮助开发者...
- 突发!Vue3 投屏神器引爆程序员朋友圈
-
【AlarmLevel】重要【AlarmTitle】突发!Vue3投屏神器引爆程序员朋友圈【AlarmOverview】最近GitHub上名为vue-screen-share的仓库突...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- 若依框架 (41)
- MVC框架 (46)
- spring框架 (46)
- 框架图 (58)
- bootstrap框架 (43)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- jpa框架 (47)
- scrapy框架 (52)
- beego框架 (42)
- java框架spring (43)
- grpc框架 (55)
- 前端框架bootstrap (42)
- orm框架有哪些 (43)
- ppt框架 (48)
- 内联框架 (52)
- winform框架 (46)
- gui框架 (44)
- cad怎么画框架 (58)
- ps怎么画框架 (47)
- ssm框架实现登录注册 (49)
- oracle v (42)
- oracle字符串长度 (48)
- oracle提交事务 (47)