[翻译] Flutter 内幕之 Elements

114 阅读12分钟

Elements

原文地址:www.flutterinternals.org/data-model/…

什么是 Elements?

  • Element tree 被锚定在 WidgetsBinding 中,并通过runApp / RenderObjectToWidgetAdapter 创建。
  • Widget 实例是 UI 配置数据的不可变的表示,这些数据被 "膨胀(inflated) "为 Element 实例(通过 Element.inflateWidget)。因此,elemet 作为widget的可变对应物,负责建模 widget 之间的关系(例如,widget 树),存储状态和继承关系,并参与构建过程等。
  • 所有 element 都与一个 BuildOwner 单例相关联。这个实例负责跟踪 dirty element,并在 WidgetsBinding.drawFrame 期间,根据需要重新构建 element tree。这个过程会触发几个生命周期事件(例如,initStatedidChangeDependenciesdidUpdateWidget)。
  • 元素被组装成一棵树(通过 Element.mountElement.unmount)。虽然这些操作是永久性的,但元素也可以被临时移除和恢复(分别通过 Element.deactivateElement.activate)。
    • Element 在几个生命周期状态(_ElementLifecycle)中转换时会相应以下几个方法:Element.mountinitialactiveElement.activate 仅在重新激活时被调用),Element.deativateactiveinactive,可用通过 Element.activate 重新激活),最后是 Element.unmountinactivedefunct
    • 请注意,deactivate 或 unmount 一个 element 是一个递归过程,一般由 build owner 中的 inactive element list 促成。所有子 element 都会受到影响(通过_InactiveElements._deactivateRecursively_InactiveElements._unmount)。
  • 当 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 主要分为 RenderObjectElementComponentElementRenderObjectElements 负责配置 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 可以有子节点。但是,在它的RenderObjectElement和与其子节点相关联的 element 之间可能有几个中间节点(即 component element)。也就是说,element tree 通常比 render tree 有更多的节点。

    • Slot token 在 element tree 上向下传递,这样这些RenderObjectElement节点就可以与其 render object 的父节点进行交互(通过RenderObjectElement.insertChildRenderObjectRenderObjectElement.moveChildRenderObjectRenderObjectElement.removeChildRenderObject)。Token 由祖先RenderObjectElement以特定的实现方式解释,以区分 render object 的子代。
  • Element 一般使用它们的 widget 的子节点作为真正的来源(例如,MultiChildRenderObjectWidget.children)。当元素第一次被挂载时,每个子节点都会被“膨胀”并存储在一个内部列表中(例如,MultiChildRenderObjectElement._children);这个列表后来在更新 element 时被使用。

  • Element 可以在一帧内从树的一个部分嫁接到另一个部分。这样的 element 会被它们的父节点 "遗忘"(通过RenderObjectElement.forgetChild),因此它们被排除在迭代和更新之外。当 element 被添加到新的父节点时,旧的父节点会移除子代(这在“膨胀”期间会发生,因为嫁接要求 widget tree 也要更新)。

  • Elements 负责更新任何子节点。为了避免不必要的“膨胀”(和潜在的 state 丢失),新旧子列表针对空列表、匹配列表和具有一个不匹配区域的列表,使用优化的线性调和方案进行同步。

  1. 将前导(leading) elements 和 widget 按 key 匹配并更新。
  2. 后面(trailing)的 element 和 widget 按键匹配,并排队更新(更新顺序很重要)。
  3. 在新旧列表中找出不匹配的区域。
  4. 旧 element 按 key 进行索引。
  5. 没有键的旧 elemnt 用 null 更新(删除)。
  6. 每新建一个不匹配的 widget,都要查询索引。
  7. 索引中带键的新 wideget 一起更新(重用)。
  8. 没有匹配的新 widget 用 null 更新(“膨胀”)。
  9. 索引中的剩余 elements 以 null 更新(删除)。

Render object element 有哪些组成部分?

  • LeafRenderObjectElementSingleChildRenderObjectElementMultiChildRenderObjectElement 为常见的用法提供支持,并对应于类似名称的 wdiget (LeafRenderObjectWidget, SingleChildRenderObjectWidget, MultiChildRenderObjectWidget)
    • 多子节点和单子节点 element 在 render tree 中对应于 ContainerRenderObjectMixinRenderObjectWithChildMixin
  • 这些 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 调用 Statebuild 方法。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(例如,StatelessWidgetStatefulWidgetProxyWidget)参与构建过程;RenderObjectWidget 子类,通常与 RenderObjectElements 相关联,不参与构建过程;她们只是在构建时更新其 render object。ComponentElement 实例只有一个子节点,通常是由其 widget 的 build 方法返回(ProxyElement返回附加到其 widget 的子节点)。
  • 当 element tree 第一次被锚定到 render tree 时(通过RenderObjectToWidgetAdapter.attachToRenderTree),RenderObjectToWidgetElement(一个 RootRenderObjectElement)为 element tree 分配一个 BuildOwnerBuildOwner 负责跟踪 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, and ComponentElement.mount。在这些情况下,目的是立即更新 element tree。
    • 其他操作则是在下一帧中进行 build。包括 State.setState, Element.reassemble, Element.didChangeDependencies, StatefulElement.activate 等。
    • Proxy element 使用 notifications 来通知底层数据的变化。 InheritedElement 的每个依赖者的 Element.didChangeDependencies 都会被调用,默认情况下,它将该 element 标记为脏。当它的任何依赖发生变化时,这将导致子节点 rebuild。
  • 每一帧,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.didChangeDependencies通知 element。默认情况下,该方法将 element 标记为 dirty。