Skip to main content

Flutter 内部机制

本文档描述了使 Flutter API 成为可能的 Flutter 工具包的内部工作原理。由于 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> 的更改可能只会触发绘制阶段,而不是构建和绘制阶段。

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

元素树和渲染对象树的分离

#

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

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

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

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

无限滚动

#

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

视口感知布局

#

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

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

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

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

按需构建小部件

#

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

交错构建和布局之所以成为可能,仅仅是因为构建和布局算法中对信息传播的严格控制。具体来说,在构建阶段,信息只能向下传播到树中。当渲染对象正在执行布局时,布局遍历尚未访问该渲染对象下方的子树,这意味着在该子树中构建生成的写入不会使迄今为止已进入布局计算的任何信息无效。类似地,一旦布局从渲染对象返回,该渲染对象在本次布局期间将永远不会再次被访问,这意味着后续布局计算生成的任何写入都不会使用于构建渲染对象的子树的信息无效。

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

API 人体工程学

#

只有当框架能够有效使用时,速度才重要。为了指导 Flutter 的 API 设计朝着更高的可用性发展,Flutter 一直在与开发人员进行广泛的用户体验研究中反复测试。这些研究有时证实了预先存在的建设计划,有时帮助指导功能的优先级排序,有时改变了 API 设计的方向。例如,Flutter 的 API 都有详细的文档;用户体验研究证实了此类文档的价值,但也强调了特别需要示例代码和说明性图表。

本节讨论了在 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() 函数。此函数接受十一个参数,其中一些参数的输入域非常宽,但它们已被仔细设计成大多彼此正交,因此几乎没有无效的组合。

积极报告错误情况

#

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

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

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

反应式范式

#

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

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

插值

#

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

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

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

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

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

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

这种方法适用范围很广。在 Flutter 中,可以插值 ColorShape 等基本类型,也可以插值更复杂的类型,例如 DecorationTextStyleTheme。这些通常由自身可以插值的组件构成,并且插值更复杂的对象通常与递归地插值描述复杂对象的所有值一样简单。

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