跳至主要内容

Flutter 内部

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

积极的组合性

#

Flutter 最显著的特点之一是其**积极的组合性**。Widget 通过组合其他 Widget 构建而成,而这些 Widget 本身又是由越来越基本的 Widget 构建而成。例如,Padding 是一个 Widget,而不是其他 Widget 的属性。因此,使用 Flutter 构建的用户界面包含许多 Widget。

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

为了支持 Widget 层的积极组合性,Flutter 在 Widget 和渲染树层都使用了一些高效的算法和优化,这些算法和优化将在以下小节中介绍。

亚线性布局

#

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

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

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

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

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

  • 每当父对象调用子对象的布局方法时,父对象都会指示它是否使用从子对象返回的大小信息。如果(通常情况下)父对象不使用大小信息,那么如果子对象选择了一个新大小,父对象无需重新计算其布局,因为父对象保证新大小将符合现有约束。

  • **紧凑**约束是指可以通过唯一一个有效几何形状满足的约束。例如,如果最小和最大宽度彼此相等,并且最小和最大高度彼此相等,则满足这些约束的唯一大小是具有该宽度和高度的大小。如果父对象提供紧凑约束,那么即使父对象的布局依赖于子对象的大小,父对象也不需要在子对象重新计算其布局时重新计算其布局,因为子对象无法在没有来自其父对象的新的约束的情况下更改大小。

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

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

亚线性 Widget 构建

#

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

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

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

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

线性协调

#

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

  • 旧子列表为空。
  • 两个列表相同。
  • 列表中恰好一个位置插入或删除了一个或多个 Widget。
  • 如果每个列表都包含一个具有相同键[5] 的 Widget,则这两个 Widget 将匹配。

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

树形手术

#

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

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

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

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

常数因子优化

#

除了这些算法优化之外,实现积极的组合性还需要依赖于几个重要的常数因子优化。这些优化在上面讨论的主要算法的叶子节点处最为重要。

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

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

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

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

总而言之,并且在积极组合创建的大型树上累加,这些优化对性能有很大影响。

Element 和 RenderObject 树的分离

#

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

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

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

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

无限滚动

#

无限滚动列表对于工具包来说是出了名的困难。Flutter 通过基于构建器模式的简单接口支持无限滚动列表,其中 ListView 使用回调根据需要构建小部件,因为它们在滚动过程中变得对用户可见。支持此功能需要视口感知布局按需构建小部件

视口感知布局

#

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

切片布局协议与盒子布局协议的结构相匹配,即父级向下传递约束给子级并接收几何信息作为返回。但是,约束和几何数据在这两种协议之间有所不同。在切片协议中,子级会获得有关视口的信息,包括剩余的可见空间量。它们返回的几何数据支持各种与滚动相关的效果,包括可折叠标题和视差。

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

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

按需构建 Widget

#

如果 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对象具有与相关控件上存在的插槽匹配的字段。例如,如果使用通用的子模型,则会强制将语义叠加到子元素列表之上,例如,将第一个子元素定义为前缀值,第二个子元素定义为后缀值,而专用的子模型则允许使用专用的命名属性。

这种灵活性允许这些树中的每个节点都以最符合其角色的方式进行操作。很少有人希望在表格中插入一个单元格,导致所有其他单元格环绕;类似地,很少有人希望通过索引而不是通过引用从弹性行中删除子元素。

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布局算法中。此布局的概念是,分配给弹性渲染对象的空间在其子元素之间分配,因此弹性的尺寸应为可用空间的全部。在原始设计中,提供无限空间将失败:这意味着弹性应无限大,这是一个无用的布局配置。相反,API进行了调整,以便当将无限空间分配给弹性渲染对象时,渲染对象会调整自身大小以适应子元素所需的大小,从而减少了可能的错误情况数量。

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

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

一个更复杂的例子是paintImage()函数。此函数接受11个参数,其中一些参数的输入域非常宽,但它们经过精心设计,彼此大多正交,因此很少有无效的组合。

积极报告错误情况

#

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

在某些情况下,这被发挥到极致:例如,在运行单元测试时,无论测试正在执行什么其他操作,每个布局的RenderBox子类都会积极检查其固有大小方法是否满足固有大小约定。这有助于捕获API中的错误,否则这些错误可能不会被执行。

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

反应式范式

#

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

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

插值

#

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

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

每个可以隐式动画化的功能都具有一个有状态的小部件,该小部件记录输入的当前值,并在输入值更改时开始动画序列,在指定持续时间内从当前值过渡到新值。

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

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

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

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

一些可插值对象由类层次结构定义。例如,形状由ShapeBorder接口表示,并且存在各种形状,包括BeveledRectangleBorderBoxBorderCircleBorderRoundedRectangleBorderStadiumBorder。单个lerp函数无法预料所有可能的类型,因此接口改为定义lerpFromlerpTo方法,静态lerp方法会委托给它们。当被告知从形状A插值到形状B时,首先询问B是否可以从AlerpFrom,然后,如果不能,则改为询问A是否可以lerpTo B。(如果两者都不可能,则函数从小于0.5的t值返回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 范围内。 ↩︎