3.2 单向绑定
> **本节目标**:完整掌握单向绑定的全部用法——文本绑定、属性绑定、表达式绑定,以及单向绑定的性能特性。
3.2.1 单向绑定的定义
单向绑定是数据从 ViewModel 流向 View 的绑定方式。ViewModel 字段变化时,View 自动更新;View 不会修改 ViewModel 字段。
<!-- 单向绑定:ViewModel → View -->
<p>{user_name}</p>
3.2.2 文本绑定
最常见的单向绑定形式,把 ViewModel 字段值显示为文本。
基础用法
<p>欢迎, {user_name}</p>
<p>当前计数: {count}</p>
<p>完成进度: {completed_count}/{total_count}</p>
字段方法调用
<p>任务总数: {todos.len()}</p>
<p>是否为空: {todos.is_empty()}</p>
<p>第一个任务: {todos.first().map(|t| t.text.as_ref()).unwrap_or("无")}</p>
嵌套字段
#[derive(IModel)]
#[component]
pub struct MyView {
pub user: User,
}
#[derive(IModel)]
pub struct User {
pub profile: UserProfile,
}
#[derive(IModel)]
pub struct UserProfile {
pub name: SharedString,
pub email: SharedString,
}
<p>姓名: {user.profile.name}</p>
<p>邮箱: {user.profile.email}</p>
3.2.3 属性绑定
把 ViewModel 字段值绑定到元素的属性。
class 绑定
<!-- 静态 class -->
<div class="card">...</div>
<!-- 动态 class:整个 class 来自 ViewModel -->
<div class={container_class}>...</div>
<!-- 混合 class:静态 + 动态 -->
<div class="card {theme_class}">...</div>
<!-- 条件 class:三元表达式 -->
<div class={is_active ? "active" : "inactive"}>...</div>
value 绑定
<input value={user_name} />
<div>{format!("¥{:.2}", price)}</div>
> ℹ️ `value={field}` 绑定到表单组件(`<input>` / `<Input>` / `<Slider>` 等)的 `pub` 可变字段时自动启用双向绑定。若绑定目标为计算表达式(如 `format!(...)`),则为单向显示。详见 [3.3 双向绑定](./two-way-binding.md)。
disabled / checked 绑定
<button disabled={is_loading}>提交</button>
<input type="checkbox" checked={remember_me} />
<input type="text" readonly={is_read_only} />
src / href 绑定
<img src={avatar_url} alt="头像" />
<a href={detail_url}>查看详情</a>
3.2.4 表达式绑定
`{ }` 内部可以是任意 Rust 表达式,只要结果是可渲染类型。
算术表达式
<p>总价: {price * quantity}</p>
<p>折扣后: {price * quantity * (1.0 - discount)}</p>
<p>平均: {total / count}</p>
逻辑表达式
<p>{is_vip ? "VIP 用户" : "普通用户"}</p>
<p>{count > 10 ? "很多" : "不多"}</p>
方法调用
<p>{todos.iter().filter(|t| t.done).count()}</p>
<p>{users.iter().map(|u| u.score).sum::<i32>()}</p>
<p>{format!("{} ({})", user.name, user.id)}</p>
if-else 表达式
<p>{
if score >= 90 { "优秀" }
else if score >= 80 { "良好" }
else if score >= 60 { "及格" }
else { "不及格" }
}</p>
⚠️ **注意**:复杂表达式应放在 `#[computed]` 方法中,避免在 `.rml` 中写过多逻辑。详见 [3.4 计算属性](./computed.md)。
3.2.5 列表项绑定
在 `each` 指令内,绑定路径是列表项本身:
<ul>
<li each={todo in todos} key={todo.id}>
<span>{todo.text}</span>
<span>{todo.done ? "✓" : "○"}</span>
</li>
</ul>
`todo` 是迭代变量,可以访问其所有 `pub` 字段。
索引绑定
<ol>
<li each={index, user in users} key={user.id}>
第 {index + 1} 名:{user.name}
</li>
</ol>
3.2.6 单向绑定的更新机制
单向绑定的更新依赖 `cx.notify()`:
#[command]
pub fn update_name(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.user_name = "新名字".into();
cx.notify(); // ← 触发所有依赖 user_name 的绑定更新
}
更新的粒度
RML 的绑定是**细粒度**的:只有依赖变化字段的 UI 元素会重新渲染。
<!-- 这两个绑定独立订阅不同字段 -->
<p>{user_name}</p> <!-- 订阅 user_name -->
<p>{count}</p> <!-- 订阅 count -->
<!-- 修改 user_name 只会更新第一个 <p> -->
批量更新
同一帧内的多次 `cx.notify()` 会被合并为一次重绘:
#[command]
pub fn batch_update(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.user_name = "新名字".into();
cx.notify(); // 不会立即重绘
self.count += 1;
cx.notify(); // 不会立即重绘
// 函数返回后,GPUI 在下一帧统一重绘
}
3.2.7 单向绑定的性能优化
1. 用计算属性替代复杂表达式
<!-- ❌ 每次重绘都重新计算 -->
<p>{items.iter().filter(|i| i.done).count()}</p>
<!-- ✅ 计算属性缓存结果 -->
<p>{completed_count}</p>
2. 用 `once` 避免不必要的订阅
<!-- 只在首次渲染时计算,之后不再更新 -->
<span once>启动时间: {start_time}</span>
3. 避免在插值中创建临时对象
<!-- ❌ 每次重绘创建新的 String -->
<p>{format!("Hello, {}", name)}</p>
<!-- ✅ 用计算属性返回 SharedString -->
<p>{greeting}</p>
#[computed]
pub fn greeting(&self) -> SharedString {
format!("Hello, {}", self.name).into()
}
3.2.8 单向绑定的限制
计算表达式不可回写
`value={expression}` 绑定计算表达式时为单向显示,用户输入不会回写:
<!-- 单向:显示格式化值,用户输入不会更新 price -->
<div>{format!("¥{:.2}", price)}</div>
表单组件绑定 `pub` 可变字段时自动双向绑定,无需额外声明:
<!-- 双向:输入回写 user_name 字段 -->
<input value={user_name} />
详见 [3.3 双向绑定](./two-way-binding.md)。
不能在插值中调用命令
<!-- ❌ 插值是纯表达式,不能有副作用 -->
<p>{submit()}</p> <!-- 编译错误 -->
3.2.9 调试单向绑定
检查绑定路径
如果绑定不显示值,检查:
1. 字段是否 `pub` 2. 字段名是否拼写正确 3. 字段类型是否可渲染
#[derive(IModel)]
#[component]
pub struct MyView {
pub user_name: SharedString, // ✅ pub
private_field: i32, // ❌ 不能绑定
}
检查 notify 调用
如果绑定值不更新,检查:
1. 修改字段后是否调用了 `cx.notify()` 2. `cx.notify()` 是否在正确的 `ViewContext` 上调用
#[command]
pub fn update(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.count += 1;
cx.notify(); // ← 必须调用
}
用 `cargo rml-expand` 查看生成代码
cargo rml-expand views::counter
这会输出 `.rml` 生成的 Rust 代码,可以检查绑定是否正确生成。详见 [第 10 章 · 调试技巧](../10-advanced/debugging.md)。
3.2.10 小结
单向绑定是 RML 最常用的绑定形式:
- **文本绑定**:`{field}` 显示字段值 - **属性绑定**:`class={}`、`value={}`、`disabled={}` 等 - **表达式绑定**:`{field + 1}`、`{field.method()}` 等 - **列表项绑定**:在 `each` 内访问迭代变量字段
记住:单向绑定是只读的,ViewModel 变化时 View 更新,但 View 不能修改 ViewModel。表单组件绑定 `pub` 可变字段时自动双向同步(详见 [3.3 双向绑定](./two-way-binding.md))。
下一节 → [3.3 双向绑定](./two-way-binding.md)