[翻译] Flutter 内幕之 Widgets

226 阅读4分钟

Widget

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

什么是 Widget?

  • Widget 提供了一个对用户界面不可变的描述。尽管 widgets 本身是不可变的,但它们可以被自由的替换、删除或重新排列(请注意,更新 widgets 的子节点通常也需要替换父 widget)。创建和销毁窗口 widget 是非常高效的,因为窗口 widget 是轻量级的,不可变的实例,理想情况下它们是编译时的常量。
  • 不可变的 widget tree 用于创建和配置(即 inflate)可变 element tree,element tree 又管理着 render tree;render tree 负责布局,绘制,手势和合成。element tree与 wideget 的更改高效同步,并在可能的情况下重用和改变 element(也就是说,尽管小部件可以用不同的实例替换,但只要两个实例具有相同的运行时类型和 key,那么原先的 element 将被更新而不是重新创建)。修改 element tree 通常会更新 render tree,而 render tree 又会改变屏幕上显示的内容。
  • 主要的 widget 类型是 RenderObjectWidgetStatefulWidgetStatelessWidget。那些将数据导出到一个或多个后代 widget(通过通知或另一种机制)的 widget 可以利用ProxyWidget 或其子类之一(例如,InheritedWidgetParentDataWidget)。
  • 通常,widget 通过修改 element tree 直接或间接配置 render object。开发者创建的大多数 widget(通过 StatefulWidgetStatelessWidget)通常通过 build 方法(例如StatelessWidget.build)委托给一组后代 widget。其他 widget(例如RenderObjectWidget)直接管理 render object(分别通过RenderObjectWidget.createRenderObjectRenderObjectWidget.updateRenderObject 创建和更新 render object)。
  • 某些 widget 通过 ProxyWidget 封装显式的子 widget,引入可继承状态(例如,InheritedWidgetInheritedModel)或配置辅助数据(例如,ParentDataWidget)。
    • ProxyWidget 通知 clients(通过 ProxyElement.notifyClients)以响应 widget 的变更(通过 ProxyElement.updated,此方法由 ProxyElement.update 调用)。
    • ParentDataWidget 更新最近的后代 render object 的 parent data(通过ParentDataElement._applyParentData,这个方法还调用了 RenderObjectElement._updateParentData);每当相应的 widget 更新时,都会触发此过程。
  • 还有一些定制的 widget 子类,它们支持一些不太常见的配置。例如,PreferredSizeWidget 继承了 Widget 并获取一个优先尺寸,从而允许子类(例如AppBarTabBarPreferredSize)向其容器(例如 Scaffold)传递尺寸信息。
  • LeafRenderObjectWidgetSingleChildRenderObjectWidgetMultiChildRenderObjectWidget 为具有零个或多个子节点的 render object widget 提供存储,而不会限制底层 render object 的创建或更新方式。这些 widget 分别对应于LeafRenderObjectElementSingleChildRenderObjectElementMultiChildRenderObjectElement,它们管理底层的位于 element tree 和 render tree 中子模型。
  • 使用 BuilderStatefulBuilder 创建匿名 widget。

Stateless Widget 是如何工作的?

  • StatelessWidgetWidget 的一个普通子类,它定义了 StatelessWidget.build 方法并配置了 StatelessElement
  • StatelessElementComponentElement 子类,该子类调用 StatelessWidget.build 来响应 StatelessElement.build(例如,将构建过程委托给它的 widget )。

Stateful Widget 是如何工作的?

  • StatefulWidget StatefulElement 相关联,StatefulElementStatelessElement 几乎相同。关键区别在于,StatefulElement 保留了对相对应的 StatefulWidgetState 的引用,并在该实例上调用了方法,而不是 widget 本身。例如,当调用 StatefulElement.update 时,通过 State.didUpdateWidget 通知 State 实例。
  • StatefulElement 在构造时创建关联的 State 实例(即StatefulWidget.createElement)。然后,当第一次创建 StatefulElement 时(通过 StatefulElement.mount 调用的 StatefulElement._firstBuild),会调用 State.initState。至关重要的是,State 实例和 StatefulWidget 引用相同的 element。
  • 由于 State 与底层的StatefulElement 相关联,因此,如果 widget 发生变化,只要 StatefulElement.updateChild能够重用同一 element(因为 widget 的 runtime type 和 key 都相同),则将保留 State。否则,将重新创建 State

为什么改变树的深度是昂贵的?

  • Flutter 没有比较树的能力。也就是说,在匹配 widget 和 element 时,只考虑元素的直系子代(通过RenderObjectElement.updateChildren)。
  • 当增加树的深度时(即插入一个中间节点),现有的父节点将配置一个与中间节点对应的子节点。在大多数情况下,这个 widget 不会对应于以前的子节点(即 Widget.canUpdate 将返回 false)。因此,新的 element 将被重新 inflated。由于这个中间节点是其父节点的子节点的新父节点,所以这些子节点中的每个子节点也将被 inflate(中间节点没有访问现有 element 的权限)。这将沿着整个子树向下进行。
  • 当减少树的深度时,父节点将再次被分配新的子节点,这些子节点很可能不会与旧的子节点同步。因此,新的子 element 将需要被 inflated,在整个子树上层层递进。
  • 给上一个子节点添加一个 GlobalKey 可以缓解这个问题,因为Element.updateChild能够重用存储在GlobalKey注册表中的 element(允许该子树简单地重新插入而不是重建)。

Notifications 是如何工作的?

  • 对 Notification 的支持不是直接建立在 widget 抽象中的,而是分层在其之上。
  • Notification 是一个抽象类,它在 element tree 中进行搜索,访问 NotificationListener 的每个子类 widget (Notification.dispatch 调用 Notification.visitAncestor 并执行此遍历)。
    • Notification 在每个适当的 widget 上调用 NotificationListener._dispatch,将通知的静态类型与回调的类型参数进行比较。如果有匹配(即 notification 是回调的类型参数的子类型),则调用 listener。
    • 如果 listener 返回 true,则遍历终止。否则,notification 会继续在树上向上冒泡。