响应式
响应式是 Slint 中一个核心概念。它允许以极少量的代码创建复杂的动态用户界面。 下面的示例将帮助你理解响应式的基础知识。
export component MyComponent { width: 400px; height: 400px;
Rectangle { background: #151515; }
ta := TouchArea {}
myRect := Rectangle { x: ta.mouse-x; y: ta.mouse-y; width: 60px; height: 60px; background: ta.pressed ? orange : skyblue; Text { x: 5px; y: 5px; text: "x: " + myRect.x / 1px; color: white; } Text { x: 5px; y: 20px; text: "y: " + myRect.y / 1px; color: white; } }}顾名思义,响应式关注的是用户界面的各个部分如何自动更新或"响应"变化。上面的示例看起来简单,但运行时它做了几件事:
Rectangle会随着鼠标移动而跟随。- 如果你在任意位置
click,Rectangle将会改变颜色。 Text元素会更新其文本以显示Rectangle的当前位置。
这种"魔法"直接内置于 Slint 语言中。你无需选择启用,也无需定义特定的有状态项。Rectangle 会自动更新,因为它的 x 和 y 属性绑定到了 TouchArea 元素的 mouse-x 和 mouse-y 属性。这是通过给 TouchArea 指定一个名称 ta 来标识它,然后在 Slint 所谓的 expression 中使用该名称来跟踪值而实现的。它简单到只需 x: ta.mouse-x; 和 y: ta.mouse-y;。mouse-x 和 mouse-y 属性内置于 TouchArea,当光标在其上移动时会自动更新。
TouchArea 还具有一个 pressed 属性,只有当光标按下或点击时它才为 true。因此三元表达式 background: ta.pressed ? orange : white; 会在 ta.pressed 为 true 时将 Rectangle 的背景色设为 orange,否则设为 white。
类似地,两个文本项也在通过跟踪矩形的 x 和 y 位置来更新。
被覆盖的绑定
在 Slint 中,绑定仅在该属性是通过绑定表达式(x: other.value)或双向绑定(<=>)赋值时才保持有效。 如果之后通过命令式赋值(例如 foo.bar = 42;)更改了属性的值,则原始绑定将被破坏。 从那时起,该属性将不再响应它先前绑定到的值的变化。 如果需要,稍后可以通过另一次赋值来更新该属性,但自动响应式就丢失了。
此行为也适用于大多数内置的 in-out 属性,例如 TextInput 的 text 属性。 当用户与控件交互(例如在输入框中输入)时,这被视为一次命令式赋值,将破坏该属性上任何现有的绑定。
若要在这种情况下保持响应式,你可以使用双向绑定(<=>), 或者利用 changed 回调来跟踪和响应属性更新。
性能
从性能角度来看,Slint 会计算出哪些属性发生了变化。然后找出所有依赖于该值的表达式。这些依赖会根据新值重新求值,UI 将随之更新。
重新求值在该属性被查询时以惰性方式发生。
在内部,当评估绑定时会为所访问的任何属性注册依赖关系。 当属性发生变化时,依赖关系会被通知,所有依赖于该属性的绑定都会被标记为 dirty。
属性表达式
表达式的复杂度可以各不相同:
// Tracks the `x` value of an element called foox: foo.x;
// Tracks the value, but sets it to 0px or 400px based on if// foo.x is greater than 400pxx: foo.x > 100px ? 0px : 400px;
// Tracks the value, but clamps it between 0px and 400pxx: clamp(foo.x, 0px, 400px);如最后一个示例所示,函数可以作为属性表达式的一部分使用。这在表达式过于复杂、无法作为单行代码阅读或维护时非常有用。
export component MyComponent { width: 400px; height: 400px;
pure function lengthToInt(n: length) -> int { return (n / 1px); }
Rectangle { background: #151515; }
ta := TouchArea {}
myRect := Rectangle { x: ta.mouse-x; y: ta.mouse-y; width: 60px; height: 60px; background: ta.pressed ? orange : skyblue; Text { x: 5px; y: 5px; text: "x: " + lengthToInt(myRect.x); color: white; } Text { x: 5px; y: 20px; text: "y: " + lengthToInt(myRect.y); color: white; } }}这里前面的示例被更新为使用一个函数将长度转换为整数。 这也使得 x 和 y 值被截断以更易读,例如显示为 "4" 而不是 "4.124488"。
纯度
对于任何响应式系统来说良好运作的前提是:评估一个属性不应改变任何可观察的状态,而只能改变该属性本身。如果是这样,那么该表达式就是"纯"的,否则就称其具有副作用。副作用是有问题的,因为并不总能明确它们何时发生:惰性求值可能会改变它们的顺序,甚至影响它们是否发生。此外,由于副作用而在绑定评估过程中对属性的更改可能导致意外行为。
因此,Slint 中的绑定必须是纯的。Slint 编译器强制要求纯上下文中的代码不得有副作用。纯上下文包括绑定表达式、纯函数体和纯回调处理函数体。在这样的上下文中,不允许修改属性,也不允许调用非纯的回调或函数。
使用 pure 关键字对回调和公共函数进行标注,以使它们可以从属性绑定以及其他纯回调和纯函数中访问。
私有函数的纯度是自动推断的。你可以将私有函数显式声明为 "pure",以让编译器强制保证其纯度。
export component Example { pure callback foo() -> int; public pure function bar(x: int) -> int { return x + foo(); }}双向绑定
使用 <=> 语法在属性之间创建双向绑定。这些属性将被链接在一起,并始终保持相同的值。也称为双向绑定。
<=> 的右侧必须是相同类型的属性的引用,或者是结构体类型的属性内同类型的字段。 在使用双向绑定时,属性类型是可选的,如果不指定则会被自动推断。 被链接属性的初始值将是绑定右侧的值。 这两个链接的属性必须在输入/输出方面兼容。
被链接属性的初始值将是绑定右侧的值。
struct Thing { name: string, price: int }
export component Example { in property<brush> rect-color <=> r.background; // It's allowed to omit the type to have it automatically inferred in property rect-color2 <=> r.background; in-out property<Thing> thing; r:= Rectangle { background: blue; } input := TextInput { // The `thing.name` field will be sync'ed with the text when the user edits text <=> root.thing.name; }}