Flutter 工作原理

本文档解释了使 Flutter API 正常工作的 Flutter 工具包内部工作原理。由于 Flutter Widget 是以积极组合的形式构建的,所以使用 Flutter 构建的用户界面含有大量 Widget。为了支撑这些负载, Flutter 使用了次线性算法来布局和构建 Widget,这些数据结构使树形结构优化更加高效,并且具有很多常量因子优化。通过一些额外的机制,该设计也允许开发者利用回调(用于构建用户可见的 Widget)来轻松创建无限滚动列表。

This document describes the inner workings of the Flutter toolkit that make Flutter’s API possible. Because Flutter widgets are built using aggressive composition, user interfaces built with Flutter have a large number of widgets. To support this workload, Flutter uses sublinear algorithms for layout and building widgets as well as data structures that make tree surgery efficient and that have a number of constant-factor optimizations. With some additional details, this design also makes it easy for developers to create infinite scrolling lists using callbacks that build exactly those widgets that are visible to the user.

积极可组合性

Aggressive composability

组合性是 Flutter 最为出众的一个特性。Widget 通过组合其他 Widget 的方式进行构建,并且这些 Widget 自身由更基础的 Widget 构建。比如,Padding 是一个 Widget 而非其他 Widget 的属性。因此,使用 Flutter 创建的用户界面是由多个 Widget 组成的。

One of the most distinctive aspects of Flutter is its aggressive composability. Widgets are built by composing other widgets, which are themselves built out of progressively more basic widgets. For example, Padding is a widget rather than a property of other widgets. As a result, user interfaces built with Flutter consist of many, many widgets.

Widget 递归构建的底层是 RenderObjectWidget,它将在渲染树的底部创建子节点。渲染树是一种存储用户界面几何信息的数据结构,该几何信息在布局期间计算并在绘制命中测试期间使用。大多数 Flutter 开发者无需直接创建这些对象,而是使用 Widget 来操纵渲染树。

The widget building recursion bottoms out in RenderObjectWidgets, which are widgets that create nodes in the underlying render tree. The render tree is a data structure that stores the geometry of the user interface, which is computed during layout and used during painting and hit testing. Most Flutter developers do not author render objects directly but instead manipulate the render tree using widgets.

为了支持 Widget 层的积极可组合性,Flutter 在 Widget 和树渲染层使用了大量的高效算法和优化措施,这些将在下面小节中进行介绍。

In order to support aggressive composability at the widget layer, Flutter uses a number of efficient algorithms and optimizations at both the widget and render tree layers, which are described in the following subsections.

次线性布局

Sublinear layout

使用大量 Widget 及渲染对象并保持高性能的关键是使用高效的算法。其中最重要的是确定渲染对象几何空间(比如大小和位置)的布局算法的性能。其他一些工具包使用 O(N²) 或更糟糕的布局算法(例如,约束域中的不动点迭代)。Flutter 的目标在于布局初始化的线性性能,及一般情况下更新现有布局的次线性布局性能。通常情况下,布局所花费的时间应该比对象渲染要多得多。

With a large number of widgets and render objects, the key to good performance is efficient algorithms. Of paramount importance is the performance of layout, which is the algorithm that determines the geometry (for example, the size and position) of the render objects. Some other toolkits use layout algorithms that are O(N²) or worse (for example, fixed-point iteration in some constraint domain). Flutter aims for linear performance for initial layout, and sublinear layout performance in the common case of subsequently updating an existing layout. Typically, the amount of time spent in layout should scale more slowly than the number of render objects.

Flutter 对每一帧执行一次布局操作,且布局算法仅在一次传递中完成。约束信息通过父节点调用每个子节点的布局方法向下传递。子节点递归执行自身的布局操作,并在它们的布局方法中返回几何信息以便将其添加到渲染树中。需要注意的是,一旦渲染对象从布局中返回,该对象将不会被再次访问 1,直到下一帧布局的执行。该策略将可能存在的单独测量和布局传递合并为单次传递,因此,每个渲染对象在布局过程中最多被访问两次 2 :一次在树的向下传递过程中,一次在树的向上传递过程中。

Flutter performs one layout per frame, and the layout algorithm works in a single pass. Constraints are passed down the tree by parent objects calling the layout method on each of their children. The children recursively perform their own layout and then return geometry up the tree by returning from their layout method. Importantly, once a render object has returned from its layout method, that render object will not be visited again1 until the layout for the next frame. This approach combines what might otherwise be separate measure and layout passes into a single pass and, as a result, each render object is visited at most twice2 during layout: once on the way down the tree, and once on the way up the tree.

针对这个通用协议,Flutter 拥有多种实现。最常用的是 RenderBox,它以二维的笛卡尔坐标进行运算。在盒子布局中,约束是最小及最大宽高。在布局过程中,子节点通过选择这些边界内的大小来确定其几何信息。子节点在布局中返回后,由父节点确定该子节点在父坐标系中的位置 3。注意,子节点的布局并不取决于它的位置,这是因为它的位置直到它从布局中返回后才确定。因此父节点可以在无需重新计算子节点布局的情况下重新定位子节点的位置信息。

Flutter has several specializations of this general protocol. The most common specialization is RenderBox, which operates in two-dimensional, cartesian coordinates. In box layout, the constraints are a min and max width and a min and max height. During layout, the child determines its geometry by choosing a size within these bounds. After the child returns from layout, the parent decides the child’s position in the parent’s coordinate system3. Note that the child’s layout cannot depend on its position, as the position is not determined until after the child returns from the layout. As a result, the parent is free to reposition the child without needing to recompute its layout.

更广泛地讲,在布局期间,从父节点流向子节点的唯一信息是约束信息,从子节点流向父节点的唯一信息是几何信息。通过这些不变量可减少布局期间所需的工作量:

More generally, during layout, the only information that flows from parent to child are the constraints and the only information that flows from child to parent is the geometry. These invariants can reduce the amount of work required during layout:

  • 如果父节点对子节点使用与上一次布局中相同的约束,且子节点没有将自己的布局标记为脏,那么该节点可立即从布局中返回,以切断布局的向下传递。

    If the child has not marked its own layout as dirty, the child can return immediately from layout, cutting off the walk, as long as the parent gives the child the same constraints as the child received during the previous layout.

  • 当父节点调用子节点的布局方法时,父节点会表明它是否使用从子节点返回的大小信息。如果父节点经常不使用此信息,即使子节点重新选择了大小,父节点依旧无需重新计算其布局,这是因为父节点需要保证新的大小符合现有约束。

    Whenever a parent calls a child’s layout method, the parent indicates whether it uses the size information returned from the child. If, as often happens, the parent does not use the size information, then the parent need not recompute its layout if the child selects a new size because the parent is guaranteed that the new size will conform to the existing constraints.

  • 严格约束是指恰好由一个有效几何满足的约束。比如,如果最小最大宽度彼此相等,且最小最大高度彼此相等,那么满足这些约束的唯一大小便是具有该宽度及高度的大小。如果父节点提供了严格约束,即便父节点在布局中使用了子节点的大小,在子节点重新计算布局时,父节点的布局也无需重新计算,这是因为子节点在没有父节点新约束的情况下无法更改其大小。

    Tight constraints are those that can be satisfied by exactly one valid geometry. For example, if the min and max widths are equal to each other and the min and max heights are equal to each other, the only size that satisfies those constraints is one with that width and height. If the parent provides tight constraints, then the parent need not recompute its layout whenever the child recomputes its layout, even if the parent uses the child’s size in its layout, because the child cannot change size without new constraints from its parent.

  • 渲染对象可以声明仅使用父节点提供的约束来确定其几何信息。此类声明通知框架:即便约束为非严格约束,以及父节点的布局取决于子节点的大小, 该渲染对象父节点的布局在子节点的布局重新计算时仍无需重新计算,这是因为子节点在没有父节点新约束的情况下无法更改其大小。

    A render object can declare that it uses the constraints provided by the parent only to determine its geometry. Such a declaration informs the framework that the parent of that render object does not need to recompute its layout when the child recomputes its layout even if the constraints are not tight and even if the parent’s layout depends on the child’s size, because the child cannot change size without new constraints from its parent.

这些优化措施的效果是,当渲染对象包含脏节点时,在布局过程中,只有这些节点以及它们周围子树的有限节点才允许被访问。

As a result of these optimizations, when the render object tree contains dirty nodes, only those nodes and a limited part of the subtree around them are visited during layout.

次线性 Widget 构建

Sublinear widget building

Flutter 使用类似于布局的次线性算法来构建 Widget。Widget 构建完成后,它们将被保留了用户页面逻辑结构的 element 树保存。element 树是非常有必要的,这是因为 Widget 自身是不可变的,这意味着(其他情况除外),它们无法记住父(或子)节点与其他 Widget 的关系。element 还保存了与 Stateful Widget 相关联的 state 对象。

Similar to the layout algorithm, Flutter’s widget building algorithm is sublinear. After being built, the widgets are held by the element tree, which retains the logical structure of the user interface. The element tree is necessary because the widgets themselves are immutable, which means (among other things), they cannot remember their parent or child relationships with other widgets. The element tree also holds the state objects associated with stateful widgets.

由于用户输入(或来自其他地方的响应),比如开发者在关联的 state 对象上调用了 setState() 方法,element 可能会变脏。框架维护了一个脏 element 列表,使得构建过程可跳过干净的 element,直接跳转到脏的 element。构建过程中,信息在 element 树中向下单向传递,这意味着该阶段中每个 element 最多会被访问一次。一个 element 一旦被清洗,它将不会再次变脏,这是因为通过归纳,它所有的祖先 element 也都是干净的 4

In response to user input (or other stimuli), an element can become dirty, for example if the developer calls setState() on the associated state object. The framework keeps a list of dirty elements and jumps directly to them during the build phase, skipping over clean elements. During the build phase, information flows unidirectionally down the element tree, which means each element is visited at most once during the build phase. Once cleaned, an element cannot become dirty again because, by induction, all its ancestor elements are also clean4.

由于 Widget 是不可变的,因此父节点使用相同的 Widget 来重新构建 element,如果 element 没有将自己标记为脏,那么该 element 可立即从构建中返回,以切断构建的向下传递。另外,element 只需比较两个 Widget 所引用的对象标识来确定新 Widget 与旧 Widget 是否相同。开发者可利用该优化实现投影模式,即 Widget 包含了被存储为成员变量、在构建过程中预先构建的子 Widget

Because widgets are immutable, if an element has not marked itself as dirty, the element can return immediately from build, cutting off the walk, if the parent rebuilds the element with an identical widget. Moreover, the element need only compare the object identity of the two widget references in order to establish that the new widget is the same as the old widget. Developers exploit this optimization to implement the reprojection pattern, in which a widget includes a prebuilt child widget stored as a member variable in its build.

构建过程中,Flutter 同时使用 InheritedWidgets 来避免父链的遍历。如果 Widget 经常遍历它们的父链,比如确定当前的主题颜色,那么构建阶段树的深底将变为 O(N²),由于 Flutter 的积极可组合性,其数量可能非常巨大。为了避免这些父链的遍历,框架通过在每个 element 上维护一个 InheritedWidget 哈希表来向下传递 element 树中的信息。通常情况下,多个 element 引用相同的哈希表,并且该表仅在 element 引入新的 InheritedWidget 时改变。

During build, Flutter also avoids walking the parent chain using InheritedWidgets. If widgets commonly walked their parent chain, for example to determine the current theme color, the build phase would become O(N²) in the depth of the tree, which can be quite large due to aggressive composition. To avoid these parent walks, the framework pushes information down the element tree by maintaining a hash table of InheritedWidgets at each element. Typically, many elements will reference the same hash table, which changes only at elements that introduce a new InheritedWidget.

线性协调

Linear reconciliation

不同于传统做法,Flutter 没有使用树差异比较算法。相反,框架通过使用 O(N) 算法独立地检查每个 element 的子节点来决定是否重用该 element。子列表协调算法针对以下情况进行了优化:

Contrary to popular belief, Flutter does not employ a tree-diffing algorithm. Instead, the framework decides whether to reuse elements by examining the child list for each element independently using an O(N) algorithm. The child list reconciliation algorithm optimizes for the following cases:

  • 旧的子列表为空。

    The old child list is empty.

  • 两个列表完全相同。

    The two lists are identical.

  • 在列表的某个位置插入或删除一个或多个 Widget。

    There is an insertion or removal of one or more widgets in exactly one place in the list.

  • 如果新旧列表都包含相同 key 的 Widget,那么这两个 Widget 就会被认为是相同的。

    If each list contains a widget with the same key, the two widgets are matched.

通常的做法是从新旧子列表的头部和尾部开始对每一个 Widget 的运行时类型和 key 进行匹配,这样就可能找到在两个列表中间所有不匹配子节点的(非空)范围。然后框架将旧子列表中该范围内的子项根据它的 key 放入一个哈希表中。接下来,框架将会遍历新的子列表以寻找该范围内能够匹配哈希表中的 key 的子项。无法匹配的子项将会被丢弃并从头开始重建,匹配到的子项则使用它们新的 Widget 进行重建。

The general approach is to match up the beginning and end of both child lists by comparing the runtime type and key of each widget, potentially finding a non-empty range in the middle of each list that contains all the unmatched children. The framework then places the children in the range in the old child list into a hash table based on their keys. Next, the framework walks the range in the new child list and queries the hash table by key for matches. Unmatched children are discarded and rebuilt from scratch whereas matched children are rebuilt with their new widgets.

树结构优化

Tree surgery

重用 element 对性能非常重要,这是因为 element 拥有两份关键数据:Stateful Widget 的状态对象及底层的渲染对象。当框架能够重用 element 时,用户界面的逻辑状态信息是不变的,并且可以重用之前计算的布局信息,这通常可以避免遍历整棵子树。事实上,重用 element 是非常有价值的,因为 Flutter 支持全局树更新,以此保留状态和布局信息。

Reusing elements is important for performance because elements own two critical pieces of data: the state for stateful widgets and the underlying render objects. When the framework is able to reuse an element, the state for that logical part of the user interface is preserved and the layout information computed previously can be reused, often avoiding entire subtree walks. In fact, reusing elements is so valuable that Flutter supports non-local tree mutations that preserve state and layout information.

开发者可通过将 GlobalKey 与其中一个 Widget 相关联来实施全局树更新。每个全局 key 在整个应用中都是唯一的,并使用特定于线程的哈希表进行注册。在构建过程中,开发者可以使用全局 key 将 Widget 移动到 element 树的任意位置。框架将不会在该位置上重新构建 element,而是检查哈希表并将现有的 element 从之前的位置移动到新的位置,从而保留整棵子树。

Developers can perform a non-local tree mutation by associating a GlobalKey with one of their widgets. Each global key is unique throughout the entire application and is registered with a thread-specific hash table. During the build phase, the developer can move a widget with a global key to an arbitrary location in the element tree. Rather than building a fresh element at that location, the framework will check the hash table and reparent the existing element from its previous location to its new location, preserving the entire subtree.

重新构建的子树中的渲染对象能够保留它们的布局信息,这是因为布局约束是渲染树从父节点传递到子节点的唯一信息。子列表发生变化后,父节点将会被标记为脏,但如果新的父节点传递给子节点的布局约束与该子节点从旧的父节点接收到的相同,那么子节点可立即从布局中返回,从而切断布局的向下传递。

The render objects in the reparented subtree are able to preserve their layout information because the layout constraints are the only information that flows from parent to child in the render tree. The new parent is marked dirty for layout because its child list has changed, but if the new parent passes the child the same layout constraints the child received from its old parent, the child can return immediately from layout, cutting off the walk.

开发者广泛使用全局 key 和全局树更新来实现 hero transition 及导航等效果。

Global keys and non-local tree mutations are used extensively by developers to achieve effects such as hero transitions and navigation.

恒定因子优化

Constant-factor optimizations

除了上述算法优化,实现积极可组合还需依赖几个重要的恒定因子优化。这些优化对于上面所讨论的主要算法是非常重要的。

In addition to these algorithmic optimizations, achieving aggressive composability also relies on several important constant-factor optimizations. These optimizations are most important at the leaves of the major algorithms discussed above.

  • 子模型无关。与大多数使用子列表的工具包不同,Flutter 渲染树不会记住一个特定的子模型。比如,类 RenderBox 存在一个抽象的 visitChildren() 方法,而非具体的 firstChildnextSibling 接口。许多子类仅支持直接作为其成员变量的单个子项,而非子项列表。比如,由于 RenderPadding 仅支持单个子节点,因此它拥有一个更为简单、高效的布局方法。

    Child-model agnostic. Unlike most toolkits, which use child lists, Flutter’s render tree does not commit to a specific child model. For example, the RenderBox class has an abstract visitChildren() method rather than a concrete firstChild and nextSibling interface. Many subclasses support only a single child, held directly as a member variable, rather than a list of children. For example, RenderPadding supports only a single child and, as a result, has a simpler layout method that takes less time to execute.

  • 视觉渲染树、Widget 逻辑树。在 Flutter 中,渲染树在与设备无关的视觉坐标系中运行,这意味着即使 x 轴的读取方向是从右到左,其左侧的值依旧小于右侧。Widget 树通常在逻辑坐标中运行,这意味着拥有开始结束值的视觉解释取决于读取方向。逻辑坐标到视觉坐标的转换是在 Widget 树和渲染树之间的切换中完成的。这种方法更为高效的原因是,渲染树中的布局和绘制计算比 Widget 到渲染树的切换更加频繁,并且可以避免重复的坐标转换。

    Visual render tree, logical widget tree. In Flutter, the render tree operates in a device-independent, visual coordinate system, which means smaller values in the x coordinate are always towards the left, even if the current reading direction is right-to-left. The widget tree typically operates in logical coordinates, meaning with start and end values whose visual interpretation depends on the reading direction. The transformation from logical to visual coordinates is done in the handoff between the widget tree and the render tree. This approach is more efficient because layout and painting calculations in the render tree happen more often than the widget-to-render tree handoff and can avoid repeated coordinate conversions.

  • 通过专门的渲染对象处理文本。大多数渲染对象都不清楚文本的复杂性。相反,文本是由专门的渲染对象 RenderParagraph 进行处理,它是渲染树中的一个叶子节点。开发者使用组合形式将文本并入到用户界面中,而非使用文本感知渲染对象进行子类化。该模式意味着 RenderParagraph 可避免文本布局在父节点提供相同布局约束下的重复计算,这是非常常见的,即使在树优化期间也是如此。

    Text handled by a specialized render object. The vast majority of render objects are ignorant of the complexities of text. Instead, text is handled by a specialized render object, RenderParagraph, which is a leaf in the render tree. Rather than subclassing a text-aware render object, developers incorporate text into their user interface using composition. This pattern means RenderParagraph can avoid recomputing its text layout as long as its parent supplies the same layout constraints, which is common, even during tree surgery.

  • 可观察对象。 Flutter 使用模型观察及响应设计模式。显而易见,响应模式占主导地位,但 Flutter 在某些叶子节点的数据结构上使用了可观察对象。比如 Animation 会在值发生变化时通知观察者列表。Flutter 将这些可观察对象从 Widget 树转移到渲染树中,渲染树直接监听这些对象,并在它们改变时仅重绘管道的相关阶段。比如,更改 Animation<Color> 可能只触发绘制阶段,而非整个构建和绘制阶段。

    Observable objects. Flutter uses both the model-observation and the reactive paradigms. Obviously, the reactive paradigm is dominant, but Flutter uses observable model objects for some leaf data structures. For example, Animations notify an observer list when their value changes. Flutter hands off these observable objects from the widget tree to the render tree, which observes them directly and invalidates only the appropriate stage of the pipeline when they change. For example, a change to an _Animation_ might trigger only the paint phase rather than both the build and paint phases.

总的来说,这些优化对通过积极组合方式产生的大型树结构的性能产生了重大影响。

Taken together and summed over the large trees created by aggressive composition, these optimizations have a substantial effect on performance.

Separation of the Element and RenderObject trees

The RenderObject and Element (Widget) trees in Flutter are isomorphic (strictly speaking, the RenderObject tree is a subset of the Element tree). An obvious simplification would be to combine these trees into one tree. However, in practice there are a number of benefits to having these trees be separate:

  • Performance. When the layout changes, only the relevant parts of the layout tree need to be walked. Due to composition, the element tree frequently has many additional nodes that would have to be skipped.

  • Clarity. The clearer separation of concerns allows the widget protocol and the render object protocol to each be specialized to their specific needs, simplifying the API surface and thus lowering the risk of bugs and the testing burden.

  • Type safety. The render object tree can be more type safe since it can guarantee at runtime that children will be of the appropriate type (each coordinate system, e.g. has its own type of render object). Composition widgets can be agnostic about the coordinate system used during layout (for example, the same widget exposing a part of the app model could be used in both a box layout and a sliver layout), and thus in the element tree, verifying the type of render objects would require a tree walk.

无限滚动

Infinite scrolling

对于工具包来说,实现无限滚动列表是非常困难的。Flutter 支持基于构造器模式实现的简单无限滚动列表界面,其中 ListView 使用回调按需构建 Widget,即它们只在滚动过程中才对用户可见。该功能需要视窗感知布局按需构建 Widget的支持。

Infinite scrolling lists are notoriously difficult for toolkits. Flutter supports infinite scrolling lists with a simple interface based on the builder pattern, in which a ListView uses a callback to build widgets on demand as they become visible to the user during scrolling. Supporting this feature requires viewport-aware layout and building widgets on demand.

视窗感知布局

Viewport-aware layout

同 Flutter 中的大多数东西一样,可滚动的 Widget 是基于组合模式构建的。可滚动 Widget 的外部是一个 Viewport,这是一个拥有更大内部空间的盒子,这意味着它的子节点可以超出视窗口的边界并滚动到可视区域中。但是,视窗口没有 RenderBox 子节点,而是拥有被称为 sliver,实现了视窗感知协议的 RenderSliver 子节点。

Like most things in Flutter, scrollable widgets are built using composition. The outside of a scrollable widget is a Viewport, which is a box that is “bigger on the inside,” meaning its children can extend beyond the bounds of the viewport and can be scrolled into view. However, rather than having RenderBox children, a viewport has RenderSliver children, known as slivers, which have a viewport-aware layout protocol.

sliver 布局协议中父节点向下传递给子节点的约束信息及接收到的几何信息的结构与盒子布局相同。但约束和几何数据在两个协议之间不同。在 sliver 协议中,子节点接收到的是关于视窗口的信息,这其中包含剩余的可见空间量。它们返回的几何数据支持各种滚动链接效果,包括可折叠标题及视差。

The sliver layout protocol matches the structure of the box layout protocol in that parents pass constraints down to their children and receive geometry in return. However, the constraint and geometry data differs between the two protocols. In the sliver protocol, children are given information about the viewport, including the amount of visible space remaining. The geometry data they return enables a variety of scroll-linked effects, including collapsible headers and parallax.

不同的 sliver 以不同的方式填充视窗口中的可用空间。比如,生成线性子列表的 sliver 按顺序排列每个子节点,直到 sliver 中无任何子节点或可用空间。同理,生成二维子节点网格的 sliver 仅填充网格中的可见区域。由于它们知道还有多大的可见空间,sliver 可以生成有限的子节点,即使它们可能生成无限的子节点。

Different slivers fill the space available in the viewport in different ways. For example, a sliver that produces a linear list of children lays out each child in order until the sliver either runs out of children or runs out of space. Similarly, a sliver that produces a two-dimensional grid of children fills only the portion of its grid that is visible. Because they are aware of how much space is visible, slivers can produce a finite number of children even if they have the potential to produce an unbounded number of children.

可组合 sliver 来创建特定的滚动布局和效果。比如,单个视窗口可以有一个折叠标题、一个线性列表和一个网格。所有这些 sliver 将按照 sliver 布局协议进行协作,只生成那些在视窗口实际可见的子节点,而不管这些子节点是否属于标题、列表或网格。

Slivers can be composed to create bespoke scrollable layouts and effects. For example, a single viewport can have a collapsible header followed by a linear list and then a grid. All three slivers will cooperate through the sliver layout protocol to produce only those children that are actually visible through the viewport, regardless of whether those children belong to the header, the list, or the grid.

按需构建 Widget

Building widgets on demand

如果 Flutter 拥有一个严格的从构建到布局,再到绘制的管道,那么前面的内容将不足以实现无限滚动列表,这是因为只有在布局阶段才能通过视窗口获取可用的空间信息。如果没有额外的机制,在布局阶段构建用于填充空间的 Widget 已经太迟了。Flutter 使用将管道的构建与布局交叉在一起的方式来解决这个问题。在布局阶段的任意时刻, 只要这些 Widget 是当前布局的渲染对象的子节点,框架就可以按需构建新的 Widget。

If Flutter had a strict build-then-layout-then-paint pipeline, the foregoing would be insufficient to implement an infinite scrolling list because the information about how much space is visible through the viewport is available only during the layout phase. Without additional machinery, the layout phase is too late to build the widgets necessary to fill the space. Flutter solves this problem by interleaving the build and layout phases of the pipeline. At any point in the layout phase, the framework can start building new widgets on demand as long as those widgets are descendants of the render object currently performing layout.

只有严格控制构建及布局中消息传播的算法,才能实现构建和布局的交叉执行。也就是说,在构建过程中,消息只能沿构建树向下传递。当渲染对象进行布局时,布局遍历过程中并没有访问该渲染对象的子树,这意味通过子树构建的写入无法使到目前为止已进入布局计算过程的任何信息失效。无独有偶,一旦布局从渲染对象中返回,在当前布局过程中,该渲染对象将永远不会被再次访问,这意味后续布局计算生成的任何写入都不会使用于构建渲染对象的子树的信息失效。

Interleaving build and layout is possible only because of the strict controls on information propagation in the build and layout algorithms. Specifically, during the build phase, information can propagate only down the tree. When a render object is performing layout, the layout walk has not visited the subtree below that render object, which means writes generated by building in that subtree cannot invalidate any information that has entered the layout calculation thus far. Similarly, once layout has returned from a render object, that render object will never be visited again during this layout, which means any writes generated by subsequent layout calculations cannot invalidate the information used to build the render object’s subtree.

此外,线性协调及树结构优化对于在滚动过程中有效更新 element,以及当 element 在视窗口边缘滚动进出视图期间修改渲染树至关重要。

Additionally, linear reconciliation and tree surgery are essential for efficiently updating elements during scrolling and for modifying the render tree when elements are scrolled into and out of view at the edge of the viewport.

人机工程 API

API Ergonomics

速度只有在框架能够被有效使用时才有意义。为了引导设计更高可用性的 Flutter API,Flutter 已经在与开发者进行的广泛用户体验研究中进行了反复测试。这些研究有时证实了已有的设计决策,有时有助于引导功能的优先级,有时会改变 API 的设计方向。比如,Flutter 的 API 文档很多,用户体验的研究不仅证实了这些文档的价值,也同时强调了示例代码及说明性图表的重要性。

Being fast only matters if the framework can actually be used effectively. To guide Flutter’s API design towards greater usability, Flutter has been repeatedly tested in extensive UX studies with developers. These studies sometimes confirmed pre-existing design decisions, sometimes helped guide the prioritization of features, and sometimes changed the direction of the API design. For instance, Flutter’s APIs are heavily documented; UX studies confirmed the value of such documentation, but also highlighted the need specifically for sample code and illustrative diagrams.

本节将要讨论 Flutter API 设计中为提高可用性所做的一些决策。

This section discusses some of the decisions made in Flutter’s API design in aid of usability.

与开发者思维模式相匹配的专项 API

Specializing APIs to match the developer’s mindset

Flutter 中 WidgetElementRenderObject 的基类节点不定义子类模型。该机制允许每个节点对适用于该节点的子模型进行定制化。

The base class for nodes in Flutter’s Widget, Element, and RenderObject trees does not define a child model. This allows each node to be specialized for the child model that is applicable to that node.

大多数 Widget 对象都有一个子 Widget 对象,因此它只暴露了一个 child 参数。一些 Widget 支持任意数量的子节点,并暴露了一个获取子节点列表的 children 参数。有些 Widget 无任何子节点、不保留内存且无任何参数。同样的,RenderObjects 暴露特定于子模型的 API。RenderImage 是一个没有子节点的叶子节点。RenderPadding 只持有一个子节点,因此它有一个指向单个子节点的指针存储空间。RenderFlex 接受任意数量的子节点,并通过链表对其进行管理。

Most Widget objects have a single child Widget, and therefore only expose a single child parameter. Some widgets support an arbitrary number of children, and expose a children parameter that takes a list. Some widgets don’t have any children at all and reserve no memory, and have no parameters for them. Similarly, RenderObjects expose APIs specific to their child model. RenderImage is a leaf node, and has no concept of children. RenderPadding takes a single child, so it has storage for a single pointer to a single child. RenderFlex takes an arbitrary number of children and manages it as a linked list.

在一些罕见情况下,将使用更复杂的子类模型。渲染对象 RenderTable 的构造函数需要使用二维数组来存储子节点,所以该类暴露了用于控制行和列数量的 getter 及 setter 方法,还有一些可以用 x、y 轴坐标来替换单个子节点的特殊方法,可通过提供一个新的子节点数组来添加新行,并用单个数组及列的个数来替换整个子节点列表。该对象并不像大多数渲染对象那样使用链表,而是使用可索引数组来实现。

In some rare cases, more complicated child models are used. The RenderTable render object’s constructor takes an array of arrays of children, the class exposes getters and setters that control the number of rows and columns, and there are specific methods to replace individual children by x,y coordinate, to add a row, to provide a new array of arrays of children, and to replace the entire child list with a single array and a column count. In the implementation, the object does not use a linked list like most render objects but instead uses an indexable array.

Chip Widget 和 InputDecoration 对象具有与其控制中的插槽相匹配的字段。如果一个通用子模型将强制语义定义在子列表之上,比如将第一个子节点定义为前缀,第二个子节点定义为后缀,那么专用子模型允许使用特有的命名属性。

The Chip widgets and InputDecoration objects have fields that match the slots that exist on the relevant controls. Where a one-size-fits-all child model would force semantics to be layered on top of a list of children, for example, defining the first child to be the prefix value and the second to be the suffix, the dedicated child model allows for dedicated named properties to be used instead.

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

This flexibility allows each node in these trees to be manipulated in the way most idiomatic for its role. It’s rare to want to insert a cell in a table, causing all the other cells to wrap around; similarly, it’s rare to want to remove a child from a flex row by index instead of by reference.

RenderParagraph 对象是最极端的情况:它有一个完全不同类型的子节点,TextSpan。在 RenderParagraph 的边界,RenderObject 树会被转换为 TextSpan 树。

The RenderParagraph object is the most extreme case: it has a child of an entirely different type, TextSpan. At the RenderParagraph boundary, the RenderObject tree transitions into being a TextSpan tree.

专门用于满足开发者期望的 API 的一切方法不仅适用于子模型。

The overall approach of specializing APIs to meet the developer’s expectations is applied to more than just child models.

专门存在一些琐碎的 Widget,以便开发者在寻找问题解决方案时能够发现并使用它们。一旦知道如何使用 Expanded 和大小为零的 SizedBox 子部件,就可以轻松地为行或列添加空格,但你会发现这种模式是没有必要的,因为搜索 space 所找到的 Spacer,它是直接使用 ExpandedSizedBox 来达到同样的效果的。

Some rather trivial widgets exist specifically so that developers will find them when looking for a solution to a problem. Adding a space to a row or column is easily done once one knows how, using the Expanded widget and a zero-sized SizedBox child, but discovering that pattern is unnecessary because searching for space uncovers the Spacer widget, which uses Expanded and SizedBox directly to achieve the effect.

同理,可以通过在构建过程中不包含 Widget 子树来轻松隐藏 Widget 子树。但开发者通常希望有一个 Widget 来执行该操作,因此 Visibility 的存在便是将此模式封装在一个简单的可重用 Widget 中。

Similarly, hiding a widget subtree is easily done by not including the widget subtree in the build at all. However, developers typically expect there to be a widget to do this, and so the Visibility widget exists to wrap this pattern in a trivial reusable widget.

明确的参数

Explicit arguments

UI 框架往往拥有大量的属性,因此很少有开发者能够记住每个类的每个构造函数参数的作用。由于 Flutter 使用响应式编程范式,因此在 Flutter 中,构建方法通常会对构造函数进行多次调用。通过利用 Dart 的命名参数,Flutter 中的 API 能够使这些构建方法保持清晰易懂。

UI frameworks tend to have many properties, such that a developer is rarely able to remember the semantic meaning of each constructor argument of each class. As Flutter uses the reactive paradigm, it is common for build methods in Flutter to have many calls to constructors. By leveraging Dart’s support for named arguments, Flutter’s API is able to keep such build methods clear and understandable.

该模式已被扩展到任何具有多个参数(尤其是具有 boolean 类型参数)的方法,因此独立的 truefalse 值在方法调用中总是自我描述的。此外,为避免 API 中通常由双重否定所造成的困惑,boolean 类型的参数和属性始终以肯定的形式命名(比如,使用 enabled: true 而非 disabled: false)。

This pattern is extended to any method with multiple arguments, and in particular is extended to any boolean argument, so that isolated true or false literals in method calls are always self-documenting. Furthermore, to avoid confusion commonly caused by double negatives in APIs, boolean arguments and properties are always named in the positive form (for example, enabled: true rather than disabled: false).

参数陷阱

Paving over pitfalls

在 Flutter 框架中被大量使用的一项技术是定义不存在错误条件的 API。这样可以避免考虑整个错误类别。

A technique used in a number of places in the Flutter framework is to define the API such that error conditions don’t exist. This removes entire classes of errors from consideration.

比如插值函数允许插值的一端或两端为空,而不是将其定义为错误:两个空值之间的插值永远为空,并且从空值或空值插值等效于对指定类型进行零模拟插值。这意味着不小心将 null 传递给插值函数的开发者不会遇到错误,而是会得到一个合理结果。

For example, interpolation functions allow one or both ends of the interpolation to be null, instead of defining that as an error case: interpolating between two null values is always null, and interpolating from a null value or to a null value is the equivalent of interpolating to the zero analog for the given type. This means that developers who accidentally pass null to an interpolation function will not hit an error case, but will instead get a reasonable result.

一个更加微妙的例子是 Flex 布局算法。该布局给予 flex 渲染对象的空间被它的子节点所划分。因此 flex 的大小应该是整个可用空间。在最初的设计中提供无限空间将导致失败:这意味着 flex 应该是无限大且无用的布局设置。然而,通过对 API 的改造,在为 flex 对象提供无限空间时,渲染对象会调整自身大小来满足所需子节点的大小,从而减少可能出现的错误次数。

A more subtle example is in the Flex layout algorithm. The concept of this layout is that the space given to the flex render object is divided among its children, so the size of the flex should be the entirety of the available space. In the original design, providing infinite space would fail: it would imply that the flex should be infinitely sized, a useless layout configuration. Instead, the API was adjusted so that when infinite space is allocated to the flex render object, the render object sizes itself to fit the desired size of the children, reducing the possible number of error cases.

该方法也可用于避免使用允许创建不符合逻辑的数据的构造函数。例如,PointerDownEvent 的构造函数不允许将 PointerEventdown 属性设置为 false(这种情况是自相矛盾的);相反,构造函数没有关于字段 down 的参数,且将值始终设置为 true

The approach is also used to avoid having constructors that allow inconsistent data to be created. For instance, the PointerDownEvent constructor does not allow the down property of PointerEvent to be set to false (a situation that would be self-contradictory); instead, the constructor does not have a parameter for the down field and always sets it to true.

一般情况下,该方法用于为输入域中的所有值定义有效的解释。最简单的例子是 Color 的构造函数。相对于接受四个整型参数(分别用于表示红色、绿色、蓝色和 alpha),其中任何一个都可能超出范围,它的默认构造函数仅接受一个整数值,并定义每位的含义(例如,低八位代表红色),以便任何输入都是有效的颜色值。

In general, the approach is to define valid interpretations for all values in the input domain. The simplest example is the Color constructor. Instead of taking four integers, one for red, one for green, one for blue, and one for alpha, each of which could be out of range, the default constructor takes a single integer value, and defines the meaning of each bit (for example, the bottom eight bits define the red component), so that any input value is a valid color value.

一个更复杂的例子是 paintImage() 函数。该函数需要 11 个参数,其中一些具有相当宽泛的输入域,但它们都经过精心设计且大部分都能够彼此相交,因此很少出现无效组合。

A more elaborate example is the paintImage() function. This function takes eleven arguments, some with quite wide input domains, but they have been carefully designed to be mostly orthogonal to each other, such that there are very few invalid combinations.

积极报告错误

Reporting error cases aggressively

并非所有的错误都能被设计出来。对于那些遗漏的错误,在 debug 版本中,Flutter 通常会尝试尽早捕获并立即报告。它使用了大量的断言,对构造函数参数进行了详细的完整性检查,并监视其生命周期,一旦检测到不一致,它们会立即引发异常。

Not all error conditions can be designed out. For those that remain, in debug builds, Flutter generally attempts to catch the errors very early and immediately reports them. Asserts are widely used. Constructor arguments are sanity checked in detail. Lifecycles are monitored and when inconsistencies are detected they immediately cause an exception to be thrown.

这在某些情况下是极端情况:比如,在执行单元测试时,无论测试用例正在做什么,每个 RenderBox 子类都会主动地检查其内部大小调整方法是否满足内部大小调整契约。这有助于捕获可能无法执行的 API 错误。

In some cases, this is taken to extremes: for example, when running unit tests, regardless of what else the test is doing, every RenderBox subclass that is laid out aggressively inspects whether its intrinsic sizing methods fulfill the intrinsic sizing contract. This helps catch errors in APIs that might otherwise not be exercised.

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

When exceptions are thrown, they include as much information as is available. Some of Flutter’s error messages proactively probe the associated stack trace to determine the most likely location of the actual bug. Others walk the relevant trees to determine the source of bad data. The most common errors include detailed instructions including in some cases sample code for avoiding the error, or links to further documentation.

响应式

Reactive paradigm

可变的基于树结构的 API 受二元访问模式的影响:创建树的原始状态通常使用与后续更新完全不同的操作集。Flutter 的渲染层使用了这种范式,因为它是维护持久树的有效方法,是高效布局和绘制的关键所在。但这也意味着,与渲染层的直接交互是十分笨拙的,甚至极其容易出错。

Mutable tree-based APIs suffer from a dichotomous access pattern: creating the tree’s original state typically uses a very different set of operations than subsequent updates. Flutter’s rendering layer uses this paradigm, as it is an effective way to maintain a persistent tree, which is key for efficient layout and painting. However, it means that direct interaction with the rendering layer is awkward at best and bug-prone at worst.

Flutter 在 Widget 层引入了一个使用响应式来操作底层渲染树的组合机制。该 API 通过将树的创建和更新步骤整合到一个单一的树结构描述(构建)中,从而将树操作抽象出来,这包括:每次系统状态更新之后,开发者用于描述用户界面的新配置;框架对于新配置所需要进行的一系列树更新计算。

Flutter’s widget layer introduces a composition mechanism using the reactive paradigm to manipulate the underlying rendering tree. This API abstracts out the tree manipulation by combining the tree creation and tree mutation steps into a single tree description (build) step, where, after each change to the system state, the new configuration of the user interface is described by the developer and the framework computes the series of tree mutations necessary to reflect this new configuration.

插值

Interpolation

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

Since Flutter’s framework encourages developers to describe the interface configuration matching the current application state, a mechanism exists to implicitly animate between these configurations.

例如,假设界面在状态 S1 由一个圆形组成,在状态 S2 时由一个正方形组成。如果没有动画机制,状态更改将导致不和谐的界面更改。隐式动画则允许界面在几个帧的时间里由圆形平滑地过渡到正方形。

For example, suppose that in state S1 the interface consists of a circle, but in state S2 it consists of a square. Without an animation mechanism, the state change would have a jarring interface change. An implicit animation allows the circle to be smoothly squared over several frames.

每个可执行隐式动画的特性都包含一个 Stateful Widget,它用于记录输入的当前值,并在输入值改变时开始执行动画序列,并在指定的持续时间内从当前值转换为新值。

Each feature that can be implicitly animated has a stateful widget that keeps a record of the current value of the input, and begins an animation sequence whenever the input value changes, transitioning from the current value to the new value over a specified duration.

这是使用不可变对象的 lerp(线性插值)函数来实现的。每个状态(这里为圆形和正方形)代表一个配置中包含恰当设置(比如颜色、笔划宽度等)且知道如何绘制自己的不可变对象。在动画绘制中间步骤时,开始和结束值连同表示动画中点的 t 值一并传递给 lerp函数。其中 0.0 代表开始,1.0 代表结束,并且该方法返回表示中间阶段的第三个不可变对象。

This is implemented using lerp (linear interpolation) functions using immutable objects. Each state (circle and square, in this case) is represented as an immutable object that is configured with appropriate settings (color, stroke width, etc) and knows how to paint itself. When it is time to draw the intermediate steps during the animation, the start and end values are passed to the appropriate lerp function along with a t value representing the point along the animation, where 0.0 represents the start and 1.0 represents the end, and the function returns a third immutable object representing the intermediate stage.

对于从圆形到正方形的转换,lerp 函数将返回一个圆角正方形对象,其半径被描述为从 t 值导出的分数,使用 lerp 函数进行插值计算的颜色,以及使用 lerp 函数进行双倍插值计算的笔划宽度。该对象与圆形、正方形一样具有相同的接口实现,并且可以在请求时进行自我绘制。

For the circle-to-square transition, the lerp function would return an object representing a “rounded square” with a radius described as a fraction derived from the t value, a color interpolated using the lerp function for colors, and a stroke width interpolated using the lerp function for doubles. That object, which implements the same interface as circles and squares, would then be able to paint itself when requested to.

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

This technique allows the state machinery, the mapping of states to configurations, the animation machinery, the interpolation machinery, and the specific logic relating to how to paint each frame to be entirely separated from each other.

在 Flutter 中,该机制得到了广泛应用。无论是像 ColorShape 这样的基本类型,还是像 DecorationTextStyleTheme 这样更为复杂的类型,都是可以进行插值处理的。它们通常是由可插入组件构成的,并且插入更复杂的对象通常就像递归插入描述复杂对象的所有值一样简单。

This approach is broadly applicable. In Flutter, basic types like Color and Shape can be interpolated, but so can much more elaborate types such as Decoration, TextStyle, or Theme. These are typically constructed from components that can themselves be interpolated, and interpolating the more complicated objects is often as simple as recursively interpolating all the values that describe the complicated objects.

一些插值对象由类层次结构定义。比如,形状由 ShapeBorder 接口表示,并且存在多种形状类型,包括: BeveledRectangleBorderBoxBorderCircleBorderRoundedRectangleBorderStadiumBorder。单一的 lerp 函数并不能了解所有可能的类型信息,因此接口定义了 lerpFromlerpTo 方法以替代静态的 lerp 方法。当被告知从形状 A 切换到 B 时,将首选询问 B 是否 lerpFrom A,如其答案为否,则询问 A 是否可以 lerpTo B(如两者的答案均为否,如果 t 的值小于 0.5 则返回 A,否则返回 B)。

Some interpolatable objects are defined by class hierarchies. For example, shapes are represented by the ShapeBorder interface, and there exists a variety of shapes, including BeveledRectangleBorder, BoxBorder, CircleBorder, RoundedRectangleBorder, and StadiumBorder. A single lerp function cannot have a priori knowledge of all the possible types, and therefore the interface instead defines lerpFrom and lerpTo methods, which the static lerp method defers to. When told to interpolate from a shape A to a shape B, first B is asked if it can lerpFrom A, then, if it cannot, A is instead asked if it can lerpTo B. (If neither is possible, then the function returns A from values of t less than 0.5, and returns B otherwise.)

这允许类层次结构的任意扩展,后续新增的能够在先前已知值与它们之间进行插值处理。

This allows the class hierarchy to be arbitrarily extended, with later additions being able to interpolate between previously-known values and themselves.

在某些情况下,插值本身不能被任何可用的类描述,并且定义一个私有类来描述中间状态。比如在 CircleBorderRoundedRectangleBorder 之间进行插值时就是如此。

In some cases, the interpolation itself cannot be described by any of the available classes, and a private class is defined to describe the intermediate stage. This is the case, for instance, when interpolating between a CircleBorder and a RoundedRectangleBorder.

该机制的另外一个优点是:它可以处理从中间态到新值的插值。比如,在圆形到正方形过渡的中途,形状可能再次改变,导致动画需要插值到一个三角形。只要该三角形类是 lerpFrom 圆形到正方形的中间类,就可以无缝进行转换。

This mechanism has one further added advantage: it can handle interpolation from intermediate stages to new values. For example, half-way through a circle-to-square transition, the shape could be changed once more, causing the animation to need to interpolate to a triangle. So long as the triangle class can lerpFrom the rounded-square intermediate class, the transition can be seamlessly performed.

结论

Conclusion

Flutter 一切都是 Widget 的口号是围绕着通过组合 Widget 来构建用户界面,Widget 又由更为基础的 Widget 构成。这种积极组合的结果是需要精心设计的算法和数据结构才能有效处理大量的 Widget。通过一些额外的机制,这些数据结构还能使开发者轻松构建无限滚动列表,以便在 Widget 可见时进行按需构建。

Flutter’s slogan, “everything is a widget,” revolves around building user interfaces by composing widgets that are, in turn, composed of progressively more basic widgets. The result of this aggressive composition is a large number of widgets that require carefully designed algorithms and data structures to process efficiently. With some additional design, these data structures also make it easy for developers to create infinite scrolling lists that build widgets on demand when they become visible.


脚注:

Footnotes:

1 至少对于布局来说。它可能会重新审视绘制、在必要时构建辅助功能树、以及必要时的命中测试。

1 For layout, at least. It might be revisited for painting, for building the accessibility tree if necessary, and for hit testing if necessary.

2 现实情况当然更复杂一些。有些布局涉及内部维度及基线测量,这涉及到相关子树的额外遍历(在最坏的情况下,使用积极缓存来降低潜在的二次性能)。但是,这些情况非常罕见。特别是在常见的 shrink-wrapping 情况下,根本不需要内部尺寸。

2 Reality, of course, is a bit more complicated. Some layouts involve intrinsic dimensions or baseline measurements, which do involve an additional walk of the relevant subtree (aggressive caching is used to mitigate the potential for quadratic performance in the worst case). These cases, however, are surprisingly rare. In particular, intrinsic dimensions are not required for the common case of shrink-wrapping.

3 严格来说,子节点的位置不是其 RenderBox 几何体的一部分,因此无需在布局期间进行实际计算。许多渲染对象隐式地将它们的单个子节点相对于它们自身的原点定位在 0,0 处,这根本不需要进行计算或存储。一些渲染对象避免计算它们子节点的位置直到最后可能需要的时刻(比如,在绘制过程中),以避免以后没有被绘制时的计算。

3 Technically, the child’s position is not part of its RenderBox geometry and therefore need not actually be calculated during layout. Many render objects implicitly position their single child at 0,0 relative to their own origin, which requires no computation or storage at all. Some render objects avoid computing the position of their children until the last possible moment (for example, during the paint phase), to avoid the computation entirely if they are not subsequently painted.

4 该规则有一个例外。正如 按需构建 Widget 中所描述的,由于布局约束的变化,一些 Widget 可以被重建。如果 Widget 在同一帧中因与此无关的原因被标记为脏,同时也由于它受布局约束的影响,该 Widget 将会被构建两次。该次冗余构建仅限于 Widget 自身,并不会影响其后代节点。

4 There exists one exception to this rule. As discussed in the Building widgets on demand section, some widgets can be rebuilt as a result of a change in layout constraints. If a widget marked itself dirty for unrelated reasons in the same frame that it also is affected by a change in layout constraints, it will be updated twice. This redundant build is limited to the widget itself and does not impact its descendants.

5 键是一个可选的与 Widget 相关联的不透明对象,它的相等操作符用于影响协调算法。

5 A key is an opaque object optionally associated with a widget whose equality operator is used to influence the reconciliation algorithm.

6 对于可访问性,并在 Widget 构建及在窗口显示的过程中为应用提供几毫米的时间,视窗口会在可见 Widget 的前后为几百个像素构建(但不进行绘制)Widget。

6 For accessibility, and to give applications a few extra milliseconds between when a widget is built and when it appears on the screen, the viewport creates (but does not paint) widgets for a few hundred pixels before and after the visible widgets.

7 该方法首次在 Facebook 的 React 框架中得到了广泛使用。

7 This approach was first made popular by Facebook’s React library.

8 实际上,允许 t 值超过 0.0-1.0 的范围,这同样适用于某些曲线。比如 elastic 缓动曲线通过短暂的过冲来表示弹跳效应。插值逻辑通常可以在适当情况下推算出起始或结束点。对于某些类型,比如在插入颜色时,t 值被有效地固定到 0.0-1.0 的范围。

8 In practice, the t value is allowed to extend past the 0.0-1.0 range, and does so for some curves. For example, the “elastic” curves overshoot briefly in order to represent a bouncing effect. The interpolation logic typically can extrapolate past the start or end as appropriate. For some types, for example, when interpolating colors, the t value is effectively clamped to the 0.0-1.0 range.