第三章:状态管理与 Signal 响应式编程
博客v1.0系列教程(Dioxus)博客 v1.0 系列教程 (Dioxus)
第三章:状态管理与 Signal 响应式编程
1. 什么是 Signal
Signal 是 Dioxus 的响应式状态单元。它像是一个可读可写的"数据盒子":
- 读:组件在渲染时读取 Signal → 建立依赖追踪
- 写:Signal 的值被修改 → 所有依赖的组件自动重渲染
let count = use_signal(|| 0);
// 读:在渲染中使用
rsx! { "{count}" } // 当前组件建立依赖
// 写:在事件中修改
onclick: move |_| count += 1 // 触发依赖组件的重渲染
2. use_signal —— 基础状态
2.1 基本用法
#[component]
fn Counter() -> Element {
let mut count = use_signal(|| 0);
rsx! {
div { class: "counter",
h2 { "计数: {count}" }
button { onclick: move |_| *count.write() += 1, "+1" }
button { onclick: move |_| count -= 1, "-1" } // 语法糖
button { onclick: move |_| count.set(0), "重置" }
}
}
}
2.2 读写方式
let mut s = use_signal(|| vec![1, 2, 3]);
// 读取(三种方式等价)
let val = s(); // 克隆值
let val = s.read(); // 返回引用
let val = s.cloned(); // 显式克隆
// 写入(三种方式等价)
s.set(new_value); // 替换为新值
*s.write() = new_value; // 获取可变引用
s.with_mut(|v| v.push(4)); // 通过闭包修改
2.3 复杂类型
struct User {
name: String,
age: u32,
email: String,
}
let mut user = use_signal(|| User {
name: "张三".to_string(),
age: 25,
email: "zhangsan@example.com".to_string(),
});
// 修改单个字段
onclick: move |_| {
user.write().age += 1;
}
// 整体替换
onclick: move |_| {
user.set(User {
name: "李四".to_string(),
age: 30,
email: "lisi@example.com".to_string(),
});
}
3. use_memo —— 派生状态
#[component]
fn PriceCalculator() -> Element {
let price = use_signal(|| 100.0);
let quantity = use_signal(|| 1);
let discount = use_signal(|| 0.1);
// 自动计算总价:price、quantity、discount 任一变化时重新计算
let total = use_memo(move || {
let subtotal = price() * quantity() as f64;
subtotal * (1.0 - discount())
});
// 格式化显示
let display = use_memo(move || format!("¥{:.2}", total()));
rsx! {
p { "单价: {price}" }
p { "数量: {quantity}" }
p { "折扣: {discount * 100:.0}%" }
p { class: "font-bold", "总价: {display}" }
button { onclick: move |_| quantity += 1, "增加" }
}
}
use_memo 的特点:
- 只在依赖值变化时重新计算
- 惰性求值:不读取就不会计算
- 适合派生数据:过滤、排序、格式化
4. use_effect —— 副作用
#[component]
fn EffectExample() -> Element {
let count = use_signal(|| 0);
// 每次 count 变化时执行
use_effect(move || {
println!("Count 已变为: {}", count());
// 可以在这里做:日志、localStorage 持久化、DOM 操作
});
// 只执行一次(空依赖)
use_effect(move || {
println!("组件挂载");
// 清理函数(可选的)
|| println!("组件卸载");
});
rsx! {
button { onclick: move |_| count += 1, "点击: {count}" }
}
}
5. 多 Signal 协作
#[component]
fn SearchFilter() -> Element {
let search = use_signal(String::new);
let category = use_signal(|| "all".to_string());
// 读取多个 Signal,组合出过滤结果
let filtered = use_memo(move || {
let q = search().to_lowercase();
let cat = category();
ALL_ARTICLES.iter()
.filter(|a| {
let match_cat = cat == "all" || a.category == cat;
let match_search = q.is_empty()
|| a.title.to_lowercase().contains(&q);
match_cat && match_search
})
.cloned()
.collect::<Vec<_>>()
});
rsx! {
input {
value: "{search}",
oninput: move |e| search.set(e.value()),
placeholder: "搜索...",
}
p { "找到 {filtered().len()} 篇" }
}
}
6. Signal 的设计原则
| 原则 | 说明 | |------|------| | 最小作用域 | Signal 尽可能靠近使用它的组件 | | 单一职责 | 一个 Signal 只存储一个相关的数据片段 | | 避免冗余 | 能用 use_memo 派生就不要另存 Signal | | 只读必要 | 只在需要的地方读取,减少重渲染范围 |
// ❌ 过度拆分
let first_name = use_signal(String::new);
let last_name = use_signal(String::new);
let full_name = use_signal(String::new); // 冗余!可以用 use_memo
// ✅ 合理拆分
let first_name = use_signal(String::new);
let last_name = use_signal(String::new);
let full_name = use_memo(move || format!("{} {}", first_name(), last_name()));
7. 对比 React
| 概念 | React | Dioxus |
|------|-------|--------|
| 局部状态 | useState | use_signal |
| 派生状态 | useMemo | use_memo |
| 副作用 | useEffect | use_effect |
| 全局状态 | Context + Redux | use_context_provider |
| 依赖追踪 | 手动声明数组 | 自动追踪(编译时) |
Dioxus 的最大优势是自动依赖追踪——你不必手动声明依赖数组,编译器会自动分析 Signal 的读写关系。
8. 小结
use_signal创建响应式状态,读写分离use_memo缓存派生值,依赖变化时自动重算use_effect处理副作用,在组件挂载/更新/卸载时执行- Signal 自动追踪依赖,无需手动声明
- 合理划分 Signal 的作用域,最小化重渲染
- 下一章将学习组件之间的通信方式
dioxussignalstatereactiveuse_memo