Elements
什么是 Elements?
- Element tree 被锚定在
WidgetsBinding
中,并通过runApp
/RenderObjectToWidgetAdapter
创建。 Widget
实例是 UI 配置数据的不可变的表示,这些数据被 "膨胀(inflated) "为Element
实例(通过Element.inflateWidget
)。因此,elemet 作为widget的可变对应物,负责建模 widget 之间的关系(例如,widget 树),存储状态和继承关系,并参与构建过程等。- 所有 element 都与一个
BuildOwner
单例相关联。这个实例负责跟踪 dirty element,并在WidgetsBinding.drawFrame
期间,根据需要重新构建 element tree。这个过程会触发几个生命周期事件(例如,initState
、didChangeDependencies
、didUpdateWidget
)。 - 元素被组装成一棵树(通过
Element.mount
和Element.unmount
)。虽然这些操作是永久性的,但元素也可以被临时移除和恢复(分别通过Element.deactivate
和Element.activate
)。- Element 在几个生命周期状态(
_ElementLifecycle
)中转换时会相应以下几个方法:Element.mount
(initial
到active
,Element.activate
仅在重新激活时被调用),Element.deativate
(active
到inactive
,可用通过Element.activate
重新激活),最后是Element.unmount
(inactive
到defunct
) - 请注意,deactivate 或 unmount 一个 element 是一个递归过程,一般由 build owner 中的 inactive element list 促成。所有子 element 都会受到影响(通过
_InactiveElements._deactivateRecursively
和_InactiveElements._unmount
)。
- Element 在几个生命周期状态(
- 当 element 首次创建时,它们被附加(即 mount)到 element tree 上。当 element 变脏时(例如,由于 widget 变更或 notification),它们可能会被多次更新(通过
Element.update
)。element 也可以被停用(deactivated);这将从 render tree 中移除任何相关的 render object,并将 element 添加到 build owner 的非活跃节点列表中。这个列表(_InactiveElements
)会自动停用受影响子树中的所有节点,并清除所有依赖关系(例如,InheritedElement
)。- 父节点通常负责停用他们的子节点(通过
Element.deactivateChild
)。停用会暂时从 element tree 中移除element(以及任何相关的 render object);解除挂载(unmount)会使这一改变成为永久性的。 - element 可以在同一帧内重新激活(例如,由于树的嫁接),否则该 element 将被构 build owner 永久地解除挂载(通过
BuildOwner.finalizeTree
调用Element.unmount
)。 - 如果 element 被重新激活,子树将被恢复并标记为 dirty,这会导致它被重建(重新采用任何之前被弃用的 render object)。
- 父节点通常负责停用他们的子节点(通过
Element.updateChild
用于在子 element 的配置(即 widget)发生变化时更新它。如果新的 widget 与旧的widget 不兼容(例如,widget 不再存在、有不同的类型或有不同的 key),一个新的 element 被”膨胀“(通过Element.inflateWidget
)。一旦元素被检索或“膨胀”,新的配置就会通过Element.update
被应用;这可能会改变一个相关的 render object、通知依赖者一个状态变化或者改变 element 本身。- 当一个 element 被重新“膨胀”时,它没有权限访问任何现有的子 element ;也就是说,与旧 element 相关联的子 element 不会传递给新的 element。因此,所有的子代也需要重新“膨胀”(没有旧 element 需要同步)。
- Global keys 是一个例外:与 Global Keys 相关联的任何子代都可以被还原,而无需重新”膨胀“。
Element 有哪些组成部分?
- Elements 主要分为
RenderObjectElement
和ComponentElement
。RenderObjectElements
负责配置 render object,并保持 render tree 和 widget tree 的同步。ComponentElements
不直接管理 render object,而是通过Widget.build
等机制产生中间节点。这两个过程都是由Element.performRebuild
驱动的,它本身是由BuildOwner.buildScope
触发的。后者作为构建过程的一部分,在每次引擎请求一个新的帧时运行。 ProxyElement
是第三类 element,它们封装着一颗 element 子树(并由ProxyWidget
配置)。这些 element 通常以某种方式增强子树(例如,InheritedElement
注入可继承的状态)。当 Proxy elements 的配置改变时,它使用通知来通知订阅者(ProxyElement.update
调用ProxyElement.updated
,后者默认调用ProxyElement.notifyClients
)。子类以特定的实现方式管理订阅者。ParentDataElement
更新所有最接近的子代 render object 的 parent data(通过ParentDataElement._applyParentData
,它被ParentDataElement.notifyClients
调用)。- 每当
InheritedElement
的配置被改变时(即当InheritedElement.update
被调用时)就会通知一组依赖者。InheritedElement._dependants
是用 Map 来实现的,因为每个依赖者可以提供一个任意的对象,以便在确定是否适用更新时使用。通过调用Element.didChangeDependencies
来通知依赖者。
RenderObjectElement
如何管理渲染树?
-
RenderObjectElement
负责管理相关的 render object。RenderObjectElement.update
将更新应用于该 render object,以匹配新的配置(即 widget)。- Render object 在其元素首次挂载时被创建(通过
RenderObjectWidget.createRenderObject
)。Render Object 在 element 的整个生命周期中都会被保留,即使元素被停用(render object 被分离)。- 如果一个 element 被”膨胀“和挂载(例如,因为一个新的 widget 无法更新旧的 widget),则会创建一个新的 render object;此时,旧的 render object 会被销毁。在这个过程中使用了一个 slot token,这样 render object 就可以从 render tree(render tree 可以与 elmenet tree 不同)中附加和分离自己。
- 当 render object的 element 第一次被挂载时,render object 会被附加到 render tree 上(通过
RenderObjectElement.attachRenderObject
)。如果 element 后来被停用(由于树的嫁接),它将在嫁接完成后被重新连接(通过RenderObjectElement.inflateWidget
,其中包括通过 global key 处理嫁接的特殊逻辑)。 - 当 render object 的 element 更新(通过
Element.update
)或重建(通过Element.rebuild
)时,render object 会被更新(通过RenderObjectWidget.updateRenderObject
)。 - 当 element 被停用时,render object 会从其父节点中分离出来(通过
RenderObjectElement.detachRenderObject
)。这通常由父节点管理(通过Element.deactivateChild
),并且发生在由于树嫁接而显式删除或重构子节点时。停用一个子节点会调用Element.detachRenderObject
,它递归地处理子节点,直到到达最近的 render object element boundary。RenderObjectElement
重写这个方法来分离它的 render object,以中断递归。
- Render object 在其元素首次挂载时被创建(通过
-
Render object 可以有子节点。但是,在它的
RenderObjectElement
和与其子节点相关联的 element 之间可能有几个中间节点(即 component element)。也就是说,element tree 通常比 render tree 有更多的节点。- Slot token 在 element tree 上向下传递,这样这些
RenderObjectElement
节点就可以与其 render object 的父节点进行交互(通过RenderObjectElement.insertChildRenderObject
、RenderObjectElement.moveChildRenderObject
、RenderObjectElement.removeChildRenderObject
)。Token 由祖先RenderObjectElement
以特定的实现方式解释,以区分 render object 的子代。
- Slot token 在 element tree 上向下传递,这样这些
-
Element 一般使用它们的 widget 的子节点作为真正的来源(例如,
MultiChildRenderObjectWidget.children
)。当元素第一次被挂载时,每个子节点都会被“膨胀”并存储在一个内部列表中(例如,MultiChildRenderObjectElement._children
);这个列表后来在更新 element 时被使用。 -
Element 可以在一帧内从树的一个部分嫁接到另一个部分。这样的 element 会被它们的父节点 "遗忘"(通过
RenderObjectElement.forgetChild
),因此它们被排除在迭代和更新之外。当 element 被添加到新的父节点时,旧的父节点会移除子代(这在“膨胀”期间会发生,因为嫁接要求 widget tree 也要更新)。 -
Elements 负责更新任何子节点。为了避免不必要的“膨胀”(和潜在的 state 丢失),新旧子列表针对空列表、匹配列表和具有一个不匹配区域的列表,使用优化的线性调和方案进行同步。
- 将前导(leading) elements 和 widget 按 key 匹配并更新。
- 后面(trailing)的 element 和 widget 按键匹配,并排队更新(更新顺序很重要)。
- 在新旧列表中找出不匹配的区域。
- 旧 element 按 key 进行索引。
- 没有键的旧 elemnt 用 null 更新(删除)。
- 每新建一个不匹配的 widget,都要查询索引。
- 索引中带键的新 wideget 一起更新(重用)。
- 没有匹配的新 widget 用 null 更新(“膨胀”)。
- 索引中的剩余 elements 以 null 更新(删除)。
Render object element 有哪些组成部分?
LeafRenderObjectElement
、SingleChildRenderObjectElement
和MultiChildRenderObjectElement
为常见的用法提供支持,并对应于类似名称的 wdiget (LeafRenderObjectWidget
,SingleChildRenderObjectWidget
,MultiChildRenderObjectWidget
)- 多子节点和单子节点 element 在 render tree 中对应于
ContainerRenderObjectMixin
和RenderObjectWithChildMixin
。
- 多子节点和单子节点 element 在 render tree 中对应于
- 这些 element 使用前一个子节点(或null)作为 slot 标识;这很便捷,因为
ContainerRenderObjectMixin
使用链表管理子节点。
ComponentElement
如何管理 element?
ComponentElement
组合其他 elements。它本身没有管理 render object ,而是在构建过程中生成子代 element,这些子 element 管理自己的 render object。- 构建是存储静态子节点列表的一种替代方式。每当 Components 变脏时,Components 就会动态地构建一个单一的子节点。
- 这个过程是由
Element.rebuild
方法驱动的,当一个 element 被标记为脏的时候,build owner 就会调用这个方法(通过BuildOwner.scheduleBuildFor
)。当 component element 第一次被挂载时(通过ComponentElement._firstBuild
)和当它们的 widget 改变时(通过ComponentElement.update
),component element 也会重新构建。对于StatefulElement
,可以通过State.setState
自动触发构建。在所有情况下,生命周期方法都会被调用以响应 element tree 的变化(例如,StatefulElement.update
将调用State.didUpdateWidget
)。 - 实际的实现是
Element.performRebuild
。Component elements 重写Element.executeRebuild
来调用ComponentElement.build
,而 RenderObjectElement 则通过RenderObjectWidget.updateRenderObject
来更新其 render object。 ComponentElement.build
提供了一个 hook 用于在 element tree 中生成中间节点。StatelessElement.build
调用 widget 的build
方法,而StatefulElement.build
调用State
的build
方法。ProxyElement
只是返回它的 widget 的子节点。- 请注意,如果一个 Component element 重新构建,子 element 和新构建的 widget 仍然会同步(通过
Element.updateChild
)。如果 widget 与现有 elment 兼容,它将被更新而不是重新“膨胀”。这允许现有的render object 被更新而不是被重新创建。根据更新的情况,这可能涉及布局、绘制和合成的任何组合。 - 重组(例如
Element.reassemble
) 将 element 标记为 dirty;大多数子类不重写这一行为。这将导致 element tree 在下一帧中被重建。RenderObjectElement 在响应Element.performRebuild
时更新它的 render object,因此也将从热重载中受益。
Build 是如何工作的?
- 只有与
ComponentElement
相关联的 widget(例如,StatelessWidget
、StatefulWidget
、ProxyWidget
)参与构建过程;RenderObjectWidget
子类,通常与RenderObjectElements
相关联,不参与构建过程;她们只是在构建时更新其 render object。ComponentElement
实例只有一个子节点,通常是由其 widget 的 build 方法返回(ProxyElement
返回附加到其 widget 的子节点)。 - 当 element tree 第一次被锚定到 render tree 时(通过
RenderObjectToWidgetAdapter.attachToRenderTree
),RenderObjectToWidgetElement
(一个RootRenderObjectElement
)为 element tree 分配一个BuildOwner
。BuildOwner
负责跟踪 dirty element (BuildOwner.scheduleBuildFor
),创建构建范围,其中的 elements 可以被 rebuild/子孙 element 可以被标记为 dirty (BuildOwner.buildScope
/BuildOwner.scheduleBuildFor
),以及在一帧结时卸载不活跃的 element (BuildOwner.finalizeTree
)。它还维护对根FocusManager
的引用,并在热重载后触发 reassemble。 - 当一个
ComponentElement
被挂载时(例如,在被”膨胀“后),会立即执行初始构建(通过ComponentElement._firstBuild
,它调用ComponentElement.rebuild
)。 - 之后,可以用
Element.markNeedsBuild
把 element 标记为 dirty。在 UI 可能需要隐式更新(或显式更恶心,响应State.setState
),都会调用这个方法。这个方法接着调用BuildOwner.onBuildScheduled
来把 element 加入到 dirty list,并通过 ``SchedulerBinding.ensureVisualUpdate` 安排一下帧的绘制。真正的 build 会在处理一下帧的时候进行。- 有的操作会直接出发 rebuild (即,无需标记树为 dirty)。这些操作包括
ProxyElement.update
,StatelessElement.update
,StatefulElement.update
, andComponentElement.mount
。在这些情况下,目的是立即更新 element tree。 - 其他操作则是在下一帧中进行 build。包括
State.setState
,Element.reassemble
,Element.didChangeDependencies
,StatefulElement.activate
等。 - Proxy element 使用 notifications 来通知底层数据的变化。
InheritedElement
的每个依赖者的Element.didChangeDependencies
都会被调用,默认情况下,它将该 element 标记为脏。当它的任何依赖发生变化时,这将导致子节点 rebuild。
- 有的操作会直接出发 rebuild (即,无需标记树为 dirty)。这些操作包括
- 每一帧,
BuildOwner.buildScope
将以深度优先的顺序遍历 element tree,只考虑那些被标记为 dirty的节点。通过锁定树并按深度优先的顺序进行迭代,任何在 rebuild 时变脏的节点都必须在树的较低位;这是因为 build 是一个单向的过程 -- 子节点不能将其父节点标记为脏。因此,不可能引入 build 循环,也不可能让已经被标记为 clean 的元素再次变为 dirty。 ComponentElement.performRebuild
委托ComponentElement.build
方法,为每个脏 element 生成一个新的 widget。接下来,Element.updateChild
被调用,以有效地重用或为子节点重新创建一个 element。至关重要的是,如果子节点的 widget 没有改变,build 就会立即中断。请注意,如果子widget
确实发生了变化,并且需要Element.update
,那么该子节点本身将被标记为 dirty,并且 build 将继续向下进行。- 每个
Element
都维护着其位置上所有InheritedElement
祖先的 map。因此,从 build 过程中访问依赖是常数级的时间复杂度。 - 如果
Element.updateChild
因为一个子元素被移除或移动到树的另一部分而调用Element.deactivateChild
,且在这一帧结束前没有重新整合,BuildOwner.finalizeTree
将卸载该 element。
Element 依赖是如何工作的?
-
InheritedElement
提供了一个有效的机制,用于向 element tree 的一个子集发布可依赖的 state。这种机制依赖于Element
本身提供的支持。 -
所有 element 都维护着一组(set)依赖项(
Element._dependencies
,例如,树中被依赖的较高的 elements)以及这个 elmenet 和根 element 之间的所有InheritedElement
实例的映射(map) (Element._inheritedWidgets
)。Dependencies set 主要是为了跟踪调试用。 -
Inherited Element 的映射是一种优化,可以避免重复遍历树。每个依赖项以它的实例类型进行唯一标识;多个依赖项共享一个类型(在这种情况下,依赖项仍然可以通过遍历树来检索)。
- 这个映射由
Element._updateInheritance
维护。默认情况下,element 会从它们的父节点中复制映射。然而,InheritedElement
实例会重写这个方法,将自己插入到映射中(映射总是被复制,因此树的不同分支是独立的)。 - 当 element 第一次被挂载(通过
Element.mount
)或重新激活(通过Element.activate
)时,这个映射是在动态建立的(通过Element._updateInheritance
)。当 element 被停用(通过Element.deactivate
)时,映射被清除;element 从其依赖的每个依赖项的依赖列表(InheritedElement._dependents
)中移除。因此,通常不需要手动访问一个 element 的祖先节点。
- 这个映射由
-
依赖关系是通过
Element.dependOnInherited
建立的(Element.inheritFromElement
是一个简单的封装)。一般来说,依赖的祖先应该在Element._inheritedWidgets
中。这样做会导致被依赖的 element 将依赖它的 element 添加到它的依赖列表中(通过InheritedElement.updateDependencies
)。- 当一个 element 重新激活时(例如,嫁接后),如果它有现有的或未满足的依赖项(例如,添加了一个依赖项,但在
Element._inheritedWidgets
中没有找到相应的InheritedElement
),它将被通知依赖项的变化。
- 当一个 element 重新激活时(例如,嫁接后),如果它有现有的或未满足的依赖项(例如,添加了一个依赖项,但在
-
当 element 的依赖项发生变化时,会通过
Element.didChangeDependencies
通知 element。默认情况下,该方法将 element 标记为 dirty。