Flutter 内部

本文档描述了 Flutter 工具包的内部工作原理,这些原理使 Flutter 的 API 成为可能。由于 Flutter 组件是使用激进组合构建的,因此使用 Flutter 构建的用户界面具有大量的组件。为了支持此工作负载,Flutter 对布局和构建组件使用亚线性算法,以及使树手术高效且具有许多常量因子优化的数据结构。通过一些其他详细信息,此设计还使开发人员可以轻松地使用回调创建无限滚动列表,这些回调仅构建对用户可见的那些组件。

激进的可组合性

Flutter 最显着的方面之一是其激进的可组合性。组件是通过组合其他组件构建的,而这些组件本身又是由更基本的组件构建的。例如,Padding 是一个组件,而不是其他组件的属性。因此,使用 Flutter 构建的用户界面由许多组件组成。

组件构建递归到底在 RenderObjectWidgets 中,这些组件在底层渲染树中创建节点。渲染树是一个数据结构,用于存储用户界面的几何形状,该几何形状在布局期间计算,并在绘制命中测试期间使用。大多数 Flutter 开发人员不会直接编写渲染对象,而是使用组件来操作渲染树。

为了在组件层支持激进的可组合性,Flutter 在组件和渲染树层使用了许多高效的算法和优化,这些算法和优化在以下小节中进行了描述。

亚线性布局

对于大量的组件和渲染对象,良好性能的关键是高效的算法。至关重要的是布局的性能,布局是确定渲染对象几何形状(例如大小和位置)的算法。一些其他工具包使用 O(N²) 或更差的布局算法(例如,某些约束域中的定点迭代)。Flutter 旨在为初始布局提供线性性能,并在随后更新现有布局的常见情况下提供亚线性布局性能。通常,在布局中花费的时间应该比渲染对象的数量增长得慢。

Flutter 每帧执行一次布局,布局算法采用单次遍历的方式。约束由父对象向下传递给树,父对象对每个子对象调用布局方法。子对象递归执行自己的布局,然后通过从其布局方法返回向上传递几何。重要的是,一旦渲染对象从其布局方法返回,该渲染对象将不会再次被访问1,直到下一帧的布局。这种方法将原本可能分开的测量和布局遍历合并为单次遍历,因此,每个渲染对象在布局期间最多被访问两次2:一次在向下遍历树时,一次在向上遍历树时。

Flutter 对此通用协议进行了若干专门化。最常见的专门化是 RenderBox,它在二维笛卡尔坐标中运行。在框布局中,约束是最大和最小宽度以及最大和最小高度。在布局期间,子对象通过在这些边界内选择一个大小来确定其几何。在子对象从布局返回后,父对象决定子对象在父对象坐标系中的位置3。请注意,子对象的布局不能依赖于其位置,因为位置是在子对象从布局返回后才确定的。因此,父对象可以自由地重新定位子对象,而无需重新计算其布局。

更一般地说,在布局期间,从父对象到子对象的唯一信息是约束,从子对象到父对象的唯一信息是几何。这些不变量可以减少布局期间所需的工作量

  • 如果子对象未将其自己的布局标记为脏,则只要父对象给子对象与子对象在上次布局期间收到的相同的约束,子对象就可以立即从布局返回,从而中断遍历。

  • 每当父级调用子级的布局方法时,父级都会指示是否使用从子级返回的大小信息。如果父级不使用大小信息(这种情况经常发生),那么当子级选择新的大小时,父级无需重新计算其布局,因为可以保证新的大小符合现有约束。

  • 严格约束是指仅能由一个有效几何形状满足的约束。例如,如果最小宽度和最大宽度相等,最小高度和最大高度相等,则满足这些约束的唯一大小是宽度和高度都相等的大小。如果父级提供严格约束,那么即使父级在其布局中使用子级的大小,父级也无需在子级重新计算其布局时重新计算其布局,因为子级无法在没有父级新约束的情况下更改大小。

  • 渲染对象可以声明它仅使用父级提供的约束来确定其几何形状。此声明通知框架,即使约束不严格,即使父级的布局依赖于子级的大小,当子级重新计算其布局时,该渲染对象的父级也不需要重新计算其布局,因为子级无法在没有父级新约束的情况下更改大小。

由于这些优化,当渲染对象树包含脏节点时,在布局期间仅访问这些节点及其周围子树的有限部分。

亚线性窗口小部件构建

与布局算法类似,Flutter 的窗口小部件构建算法是亚线性的。构建后,窗口小部件由元素树持有,该树保留用户界面的逻辑结构。元素树是必需的,因为窗口小部件本身是不可变的,这意味着(除其他事项外)它们无法记住它们与其他窗口小部件的父级或子级关系。元素树还持有与有状态窗口小部件关联的状态对象。

响应用户输入(或其他刺激),元素可能会变脏,例如,如果开发者对关联的状态对象调用 setState()。框架会保留一个脏元素列表,并在构建阶段直接跳转到这些元素,跳过干净的元素。在构建阶段,信息单向向下流经元素树,这意味着每个元素在构建阶段最多被访问一次。一旦被清理,元素就不能再次变脏,因为通过归纳,它的所有祖先元素也是干净的4

由于窗口小部件是不可变的,因此如果元素没有将自身标记为脏,那么如果父级使用相同的窗口小部件重建元素,元素可以立即从构建中返回,从而中断遍历。此外,元素只需要比较两个窗口小部件引用的对象标识,即可确定新窗口小部件与旧窗口小部件相同。开发者利用此优化来实现重新投影模式,其中窗口小部件包含一个预构建的子窗口小部件,该子窗口小部件存储在其构建中的成员变量中。

在构建期间,Flutter 还避免使用 InheritedWidgets 遍历父链。如果小组件通常遍历其父链,例如确定当前主题颜色,则构建阶段将在树的深度中变为 O(N²),由于激进的组合,这可能会非常大。为了避免这些父遍历,框架通过在每个元素中维护一个 InheritedWidget 的哈希表,将信息向下推送到元素树。通常,许多元素将引用同一个哈希表,该哈希表仅在引入新 InheritedWidget 的元素处发生更改。

线性调和

与流行的看法相反,Flutter 不使用树差异算法。相反,框架通过使用 O(N) 算法独立检查每个元素的子列表来决定是否重用元素。子列表调和算法针对以下情况进行优化

  • 旧子列表为空。
  • 这两个列表相同。
  • 在列表中的一个地方插入或删除了一个或多个小组件。
  • 如果每个列表都包含具有相同键5 的小组件,则匹配这两个小组件。

一般方法是通过比较每个小组件的运行时类型和键,将两个子列表的开头和结尾匹配起来,有可能在每个列表的中间找到一个非空范围,其中包含所有不匹配的子元素。然后,框架将旧子列表中范围内的子元素放入基于其键的哈希表中。接下来,框架遍历新子列表中的范围,并按键查询哈希表以查找匹配项。不匹配的子元素将被丢弃并从头开始重建,而匹配的子元素将使用其新小组件重建。

树形手术

重用元素对于性能很重要,因为元素拥有两条关键数据:有状态小组件的状态和底层渲染对象。当框架能够重用元素时,用户界面逻辑部分的状态将被保留,并且可以重用先前计算的布局信息,通常可以避免整个子树遍历。事实上,重用元素非常有价值,以至于 Flutter 支持保留状态和布局信息的非本地树形突变。

开发人员可以通过将 GlobalKey 与其一个小组件关联来执行非本地树形突变。每个全局键在整个应用程序中都是唯一的,并注册到特定于线程的哈希表中。在构建阶段,开发人员可以将具有全局键的小组件移动到元素树中的任意位置。框架不会在该位置构建新元素,而是将检查哈希表并将现有元素从其先前位置重新关联到其新位置,从而保留整个子树。

重新关联的子树中的渲染对象能够保留其布局信息,因为布局约束是渲染树中唯一从父级流向子级的的信息。新父级因其子列表已更改而标记为布局脏,但如果新父级传递给子级与子级从其旧父级接收的布局约束相同,则子级可以立即从布局返回,从而切断遍历。

开发人员广泛使用全局键和非本地树突变来实现英雄过渡和导航等效果。

常量因子优化

除了这些算法优化之外,实现激进的可组合性还依赖于一些重要的常量因子优化。这些优化在上面讨论的主要算法的叶子上最重要。

  • 子模型不可知。与大多数使用子列表的工具包不同,Flutter 的渲染树不会提交到特定的子模型。例如,RenderBox 类有一个抽象的 visitChildren() 方法,而不是一个具体的 firstChildnextSibling 接口。许多子类仅支持一个子类,直接作为成员变量持有,而不是一个子类列表。例如,RenderPadding 仅支持一个子类,因此具有一个更简单的布局方法,执行时间更短。

  • 可视渲染树,逻辑小部件树。在 Flutter 中,渲染树在设备无关的可视坐标系中运行,这意味着 x 坐标中的较小值始终朝向左侧,即使当前阅读方向是从右到左。小部件树通常在逻辑坐标中运行,这意味着具有开始结束值,其可视解释取决于阅读方向。从逻辑坐标到可视坐标的转换在小部件树和渲染树之间的移交中完成。这种方法更有效,因为渲染树中的布局和绘制计算比小部件到渲染树的移交更频繁,并且可以避免重复的坐标转换。

  • 由专门的渲染对象处理文本。绝大多数渲染对象都忽略文本的复杂性。相反,文本由一个专门的渲染对象 RenderParagraph 处理,它是渲染树中的一个叶节点。开发人员不是对文本感知的渲染对象进行子类化,而是使用组合将文本纳入其用户界面。此模式意味着 RenderParagraph 可以避免重新计算其文本布局,只要其父级提供相同的布局约束,这很常见,即使在树形手术期间也是如此。

  • 可观察对象。Flutter 同时使用模型观察和响应式范例。显然,响应式范例占主导地位,但 Flutter 使用可观察模型对象作为一些叶数据结构。例如,Animation 在其值更改时通知观察者列表。Flutter 将这些可观察对象从小部件树移交给渲染树,后者直接观察它们,并在它们更改时仅使管道中的适当阶段无效。例如,对 Animation<Color> 的更改可能仅触发绘制阶段,而不是构建和绘制阶段。

这些优化加在一起,并对激进组合创建的大型树进行求和,对性能有实质性影响。

Element 和 RenderObject 树的分离

Flutter 中的 RenderObjectElement(小部件)树是同构的(严格来说,RenderObject 树是 Element 树的子集)。一个显而易见的简化方法是将这些树合并为一棵树。然而,在实践中,将这些树分开有很多好处

  • 性能。当布局发生变化时,只需要遍历布局树的相关部分。由于组合,元素树通常有许多必须跳过的附加节点。

  • 清晰度。更清晰的关注点分离允许小部件协议和渲染对象协议针对其特定需求进行专门化,从而简化 API 表面,进而降低错误风险和测试负担。

  • 类型安全。渲染对象树可以更具类型安全性,因为它可以在运行时保证子项具有适当的类型(每个坐标系,例如,都有其自己的渲染对象类型)。组合小部件可以不关心布局期间使用的坐标系(例如,在盒布局和 sliver 布局中都可以使用公开应用程序模型一部分的相同小部件),因此在元素树中,验证渲染对象的类型需要遍历树。

无限滚动

无限滚动列表对于工具包来说是出了名的困难。Flutter 支持基于生成器模式的简单界面的无限滚动列表,其中 ListView 使用回调在滚动期间对用户可见时按需生成小部件。支持此功能需要视口感知布局按需生成小部件

视口感知布局

与 Flutter 中的大多数内容一样,可滚动小部件是使用组合构建的。可滚动小部件的外部是一个 Viewport,它是一个“内部更大”的盒子,这意味着它的子项可以超出视口的边界并可以滚动到视图中。但是,视口没有 RenderBox 子项,而是有 RenderSliver 子项,称为sliver,它们具有视口感知布局协议。

sliver 布局协议与盒布局协议的结构相匹配,因为父级将约束传递给子级并接收几何形状作为回报。但是,约束和几何数据在这两个协议之间有所不同。在 sliver 协议中,会向子项提供有关视口的信息,包括剩余的可见空间量。他们返回的几何数据支持各种与滚动链接的效果,包括可折叠标题和视差。

不同的 sliver 以不同的方式填充视口中可用的空间。例如,生成子项线性列表的 sliver 会按顺序排列每个子项,直到 sliver 用完子项或用完空间。类似地,生成二维子项网格的 sliver 只会填充其网格中可见的部分。因为 sliver 知道有多少空间可见,所以即使它们有可能生成无限数量的子项,它们也可以生成有限数量的子项。

Slivers 可以组合起来创建定制的可滚动布局和效果。例如,单个视口可以有一个可折叠的标题,后面是一个线性列表,然后是一个网格。所有三个 Slivers 都将通过 Sliver 布局协议进行协作,以仅生成实际上通过视口可见的那些子项,而不管这些子项属于标题、列表还是网格6

按需构建小部件

如果 Flutter 有一个严格的先构建后布局再绘制管道,那么上述内容将不足以实现无限滚动列表,因为有关通过视口可见多少空间的信息仅在布局阶段可用。如果没有额外的机制,布局阶段就太迟了,无法构建填充空间所需的小部件。Flutter 通过交错管道中的构建和布局阶段来解决此问题。在布局阶段的任何时候,框架都可以按需开始构建新小部件只要这些小部件是当前执行布局的渲染对象的子项即可

交错构建和布局只有在对构建和布局算法中的信息传播进行严格控制时才有可能。具体来说,在构建阶段,信息只能向下传播到树中。当渲染对象执行布局时,布局遍历尚未访问该渲染对象下面的子树,这意味着在该子树中构建而生成的写入不会使到目前为止进入布局计算的任何信息无效。类似地,一旦布局从渲染对象返回,该渲染对象将在此布局期间不再被访问,这意味着由后续布局计算生成的任何写入都无法使用于构建渲染对象子树的信息无效。

此外,线性协调和树形手术对于在滚动期间有效更新元素以及在元素滚动到视口边缘的视图中和视图外时修改渲染树至关重要。

API 人体工程学

只有在框架能够有效使用的情况下,快速才重要。为了指导 Flutter 的 API 设计朝着更大的可用性发展,Flutter 已在与开发人员进行的广泛 UX 研究中反复进行测试。这些研究有时会确认已有的设计决策,有时有助于指导功能的优先级,有时会改变 API 设计的方向。例如,Flutter 的 API 都有大量的文档;UX 研究证实了此类文档的价值,但也特别强调了示例代码和说明性图表的需求。

本节讨论了 Flutter 的 API 设计中为提高可用性而做出的部分决策。

专门化 API 以匹配开发人员的心态

Flutter 的 WidgetElementRenderObject 树中的节点的基类未定义子模型。这允许每个节点针对适用于该节点的子模型进行专门化。

大多数 Widget 对象都有一个子 Widget,因此只公开一个 child 参数。一些小部件支持任意数量的子项,并公开一个 children 参数,该参数采用列表。一些小部件根本没有子项并且不保留任何内存,也没有针对它们的任何参数。同样,RenderObjects 公开特定于其子模型的 API。RenderImage 是一个叶节点,没有子项的概念。RenderPadding 采用一个子项,因此它有存储空间来存储指向单个子项的单个指针。RenderFlex 采用任意数量的子项,并将其作为链表进行管理。

在一些罕见的情况下,会使用更复杂的子模型。RenderTable 渲染对象的构造函数采用一个子项数组数组,该类公开控制行数和列数的 getter 和 setter,并且有特定方法按 x,y 坐标替换单个子项,添加一行,提供一个新的子项数组数组,以及用一个数组和一个列数替换整个子项列表。在实现中,该对象不使用像大多数渲染对象那样的链表,而是使用可索引数组。

Chip 小部件和 InputDecoration 对象有与相关控件上存在的插槽匹配的字段。例如,一个通用的子模型会强制语义分层在子项列表之上,定义第一个子项为前缀值,第二个子项为后缀,而专用子模型允许使用专用命名属性。

这种灵活性允许以最符合其角色的方式操作这些树中的每个节点。很少需要在表格中插入一个单元格,导致所有其他单元格环绕它;同样,很少需要按索引而不是按引用从 flex 行中删除子项。

RenderParagraph 对象是最极端的案例:它有一个完全不同类型的子项 TextSpan。在 RenderParagraph 边界处,RenderObject 树过渡为 TextSpan 树。

将 API 专业化以满足开发人员期望的总体方法不仅适用于子模型。

一些相当琐碎的小部件专门存在,以便开发人员在寻找问题的解决方案时可以找到它们。一旦知道如何使用 Expanded 小部件和零大小的 SizedBox 子项,就可以轻松地向行或列添加一个空格,但发现该模式是不必要的,因为搜索 space 会发现 Spacer 小部件,它直接使用 ExpandedSizedBox 来实现效果。

类似地,通过根本不将小部件子树包含在构建中,可以轻松地隐藏小部件子树。但是,开发人员通常希望有一个小部件来执行此操作,因此 Visibility 小部件存在于将此模式包装在一个琐碎的可重用小部件中。

显式参数

UI 框架往往有很多属性,以至于开发人员很少能够记住每个类的每个构造函数参数的语义含义。由于 Flutter 使用响应式范例,因此 Flutter 中的构建方法通常有很多对构造函数的调用。通过利用 Dart 对命名参数的支持,Flutter 的 API 能够保持此类构建方法清晰易懂。

此模式扩展到具有多个参数的任何方法,特别是扩展到任何布尔参数,以便在方法调用中孤立的 truefalse 文字始终是自文档化的。此外,为了避免 API 中双重否定的常见混淆,布尔参数和属性始终以肯定形式命名(例如,enabled: true 而不是 disabled: false)。

铺平陷阱

Flutter 框架中许多地方使用的一种技术是定义 API,以便不存在错误条件。这消除了对错误的整个类别的考虑。

例如,插值函数允许插值的任一端或两端都为 null,而不是将其定义为错误情况:在两个 null 值之间插值始终为 null,从 null 值或到 null 值的插值等同于插值到给定类型的零模拟值。这意味着意外将 null 传递给插值函数的开发人员不会遇到错误情况,而是会得到合理的结果。

一个更微妙的例子在 Flex 布局算法中。此布局的概念是将赋予 flex 渲染对象的空格分配给其子项,因此 flex 的大小应该是可用空间的全部。在原始设计中,提供无限空间会失败:这意味着 flex 应该是无限大小,这是一种无用的布局配置。相反,API 已调整,以便当将无限空间分配给 flex 渲染对象时,渲染对象会调整自身大小以适应子项的所需大小,从而减少可能的错误情况数量。

该方法还用于避免创建允许创建不一致数据的构造函数。例如,PointerDownEvent 构造函数不允许将 PointerEventdown 属性设置为 false(这种情况会自相矛盾);相反,构造函数没有 down 字段的参数,并始终将其设置为 true

一般来说,该方法是为输入域中的所有值定义有效的解释。最简单的示例是 Color 构造函数。它不是获取四个整数(一个用于红色,一个用于绿色,一个用于蓝色,一个用于 alpha),每个整数都可能超出范围,而是获取一个单一的整数值,并定义每一位的含义(例如,底部的八位定义红色分量),以便任何输入值都是有效的颜色值。

一个更详细的示例是 paintImage() 函数。此函数获取 11 个参数,其中一些参数的输入域非常宽,但它们经过精心设计,彼此大部分正交,因此无效组合非常少。

积极报告错误情况

并非所有错误条件都可以设计出来。对于那些仍然存在的问题,在调试版本中,Flutter 通常会尝试尽早捕获错误并立即报告错误。广泛使用断言。详细检查构造函数参数的健全性。监控生命周期,当检测到不一致时,它们会立即导致抛出异常。

在某些情况下,这会被推向极端:例如,在运行单元测试时,无论测试在执行什么其他操作,每个布局的 RenderBox 子类都会积极检查其内在大小方法是否满足内在大小约定。这有助于捕获 API 中的错误,而这些错误可能无法通过其他方式来发现。

抛出异常时,它们会包含尽可能多的信息。Flutter 的一些错误消息会主动探测关联的堆栈跟踪,以确定实际错误最可能的位置。其他错误消息会遍历相关树,以确定错误数据的来源。最常见的错误包括详细说明,在某些情况下包括避免错误的示例代码,或指向进一步文档的链接。

响应式范例

可变基于树的 API 存在二分访问模式:创建树的原始状态通常使用与后续更新完全不同的操作集。Flutter 的渲染层使用此范例,因为这是维护持久树的有效方法,这对于高效布局和绘制至关重要。但是,这意味着与渲染层的直接交互充其量很尴尬,充其量容易出错。

Flutter 的小部件层引入了一种使用响应式范例7 的组合机制来操作底层渲染树。此 API 通过将树创建和树突变步骤合并到单个树描述(构建)步骤中,抽象出树操作,在该步骤中,在每次更改系统状态后,用户界面新配置由开发人员描述,框架计算反映此新配置所需的一系列树突变。

插值

由于 Flutter 的框架鼓励开发人员描述与当前应用程序状态匹配的界面配置,因此存在一种机制可在这些配置之间隐式执行动画。

例如,假设在状态 S1 中,界面由一个圆圈组成,但在状态 S2 中,它由一个正方形组成。如果没有动画机制,状态更改将导致界面发生不和谐的更改。隐式动画允许圆圈在几帧内平滑地变成正方形。

可以隐式动画的每个特性都有一个有状态小部件,它会记录输入当前值,并在输入值发生变化时开始动画序列,在指定持续时间内从当前值过渡到新值。

这是使用 lerp(线性插值)函数和不可变对象实现的。每种状态(本例中为圆形和正方形)表示为不可变对象,该对象使用适当的设置(颜色、描边宽度等)进行配置,并且知道如何绘制自身。当需要在动画过程中绘制中间步骤时,开始值和结束值将传递给适当的 lerp 函数,同时传递一个表示动画中点的 t 值,其中 0.0 表示 start,1.0 表示 end8,函数返回表示中间阶段的第三个不可变对象。

对于圆形到正方形的过渡,lerp 函数将返回一个表示“圆角正方形”的对象,其半径描述为从 t 值派生的分数,颜色使用颜色的 lerp 函数进行插值,描边宽度使用双精度值的 lerp 函数进行插值。然后,该对象(它实现了与圆形和正方形相同的界面)在需要时可以绘制自身。

此技术允许状态机制、状态到配置的映射、动画机制、插值机制以及与如何绘制每个帧相关的特定逻辑完全彼此分离。

此方法具有广泛的适用性。在 Flutter 中,ColorShape 等基本类型可以进行插值,但更精细的类型(例如 DecorationTextStyleTheme)也可以进行插值。这些类型通常由可以自身进行插值的组件构建,而对更复杂的对象进行插值通常与递归插值描述复杂对象的全部值一样简单。

一些可插值对象由类层次结构定义。例如,形状由 ShapeBorder 接口表示,并且存在各种形状,包括 BeveledRectangleBorderBoxBorderCircleBorderRoundedRectangleBorderStadiumBorder。单个 lerp 函数无法预料所有可能的类型,因此该接口定义了 lerpFromlerpTo 方法,静态 lerp 方法会推迟到它们。当告知从形状 A 插值到形状 B 时,首先询问 B 是否可以 lerpFrom A,然后,如果它不能,则询问 A 是否可以 lerpTo B。(如果两者都不可能,则该函数会返回 t 小于 0.5 的 A 值,否则返回 B。)

这允许类层次结构任意扩展,并且稍后添加的内容能够在先前已知的值和它们自身之间进行插值。

在某些情况下,插值本身无法由任何可用类描述,并且定义了一个私有类来描述中间阶段。例如,在 CircleBorderRoundedRectangleBorder 之间进行插值时就是这样。

此机制具有一个进一步的附加优势:它可以处理从中间阶段到新值的插值。例如,在圆形到方形过渡过程中,形状可以再次改变,导致动画需要插值到三角形。只要三角形类可以 lerpFrom 圆角方形中间类,就可以无缝执行过渡。

结论

Flutter 的标语“一切皆为小部件”围绕通过组合小部件来构建用户界面,而这些小部件又由更基本的小部件组成。这种激进组合的结果是大量小部件,需要精心设计算法和数据结构才能有效处理。通过一些额外的设计,这些数据结构还使开发人员可以轻松创建无限滚动列表,这些列表在可见时按需构建小部件。


脚注

1 至少对于布局而言。如果需要,可以重新审视绘制、构建无障碍树和命中测试。

2 当然,现实情况要复杂一些。一些布局涉及固有尺寸或基线测量,这确实涉及相关子树的附加遍历(使用激进缓存来减轻最坏情况下二次性能的可能性)。然而,这些情况出奇地罕见。特别是,对于收缩包装的常见情况,不需要固有尺寸。

3 从技术上讲,子元素的位置不是其 RenderBox 几何的一部分,因此实际上不必在布局期间计算。许多渲染对象隐式地将它们的单个子元素定位在它们自己的原点相对于 0,0 的位置,这根本不需要任何计算或存储。一些渲染对象避免计算其子元素的位置,直到最后可能的时刻(例如,在绘制阶段),以完全避免计算,如果它们随后没有被绘制。

4 此规则有一个例外。如 按需构建小组件 部分所述,一些小组件可以因布局约束的更改而被重建。如果一个小组件在同一帧中因无关原因将自身标记为脏,并且也受到布局约束更改的影响,则它将被更新两次。此冗余构建仅限于小组件本身,不会影响其后代。

5 键是不透明对象,可以根据需要与小组件关联,其相等运算符用于影响协调算法。

6 为了无障碍,并为应用程序在小组件构建和在屏幕上显示之间提供几毫秒的额外时间,视口会在可见小组件前后创建(但不绘制)数百像素的小组件。

7 这种方法首先由 Facebook 的 React 库推广开来。

8 在实践中,t 值允许超出 0.0-1.0 范围,并且对于某些曲线是这样做的。例如,“弹性”曲线短暂地超调,以表示反弹效果。插值逻辑通常可以根据需要推断出开始或结束。对于某些类型,例如在插值颜色时,t 值实际上被限定在 0.0-1.0 范围内。