Dioxus状态管理
一、为什么需要“响应式”?
想象你正在用 Excel 做一个简单的计算器:
A 列(输入) | B 列(公式) |
---|---|
10 | =A1 * 2 |
当你把 A1 改成 20,B1 自动变成 40。 这就是“响应式”——数据变了,依赖它的结果自动更新。
在传统前端(比如 jQuery),你要手动写:
$("#input").on("change", () => {$("#output").text(2 * getValue());
});
——繁琐、容易漏、难维护。
而 Dioxus(以及 React、Vue 等现代框架)的目标是:你只描述“当前状态应该显示什么”,框架自动处理“怎么更新”。
二、Dioxus 响应式的核心:Signal(信号)
1. 什么是 Signal?
Signal
是 Dioxus 中最基本的可变状态容器。你可以把它想象成一个带“监听器”的智能盒子:
- 盒子里装着一个值(比如数字、字符串、结构体)。
- 每次有人读这个盒子,Dioxus 就记下:“XXX 用了这个盒子”。
- 每次有人写这个盒子,Dioxus 就通知所有“用过它的人”:“值变了,快更新!”
2. 如何创建和使用?
let mut count = use_signal(|| 0); // 创建一个初始值为 0 的 Signal
读取值(三种方式):
let val1 = count(); // 拿出一个副本(最常用)
let val2 = *count.read(); // 借引用读(适合大对象,避免克隆)
log!("{count}"); // 自动调用 Display(如果实现了)
🔍 关键机制:只要你在“响应式作用域”(比如组件、use_effect、use_memo)里调用
count()
,Dioxus 就会自动记录依赖。
修改值(两种方式):
count.set(5); // 直接替换为新值
*count.write() += 1; // 获取可变引用,原地修改
💡 小技巧:
count += 1
也能用!因为 Dioxus 为 Signal 实现了AddAssign
等 trait。
三、自动执行的逻辑:use_effect
1. 它是什么?
use_effect
是一个副作用钩子,用于执行“当某些状态变化时,我要做点什么”。
“副作用” = 不是直接生成 UI 的操作,比如:发日志、发请求、操作 DOM、订阅事件等。
2. 工作原理
use_effect(move || {log!("当前计数:{}", count());
});
- Dioxus 在运行这个闭包时,会追踪里面所有 Signal 的读取。
- 它发现你读了
count()
,于是把count
记为依赖项。 - 下次
count
改变 → 自动重新运行这个闭包。
3. 常见用途
- 打印日志调试
- 发送分析事件(如“用户点击了按钮”)
- 启动/清理定时器
- 与非响应式系统交互(如 JS API)
⚠️ 注意:不要在
use_effect
里直接修改 Signal(可能引起无限循环)!如果需要,用.peek()
或加条件判断。
四、派生状态:use_memo(高效计算)
1. 为什么需要它?
假设你有:
let expensive_value = count() * count() * count(); // 三次方
每次组件重绘,都重新算一遍——浪费!
use_memo
就是带缓存的计算:只有输入变了,才重新算。
2. 使用方式
let cube = use_memo(move || count() * count() * count());
- Dioxus 会追踪闭包内所有 Signal(这里是
count
)。 - 第一次运行:计算并缓存结果。
- 后续:如果
count
没变 → 直接返回缓存值。 - 如果
count
变了 → 重新计算,并用PartialEq
比较新旧结果:- 如果相等(比如 2³=8,3³=27 → 不等),才触发下游更新;
- 如果相等(比如从 4→5,但整数除法
count()/2
都是 2),则不更新。
优势:避免不必要的 UI 重绘或计算。
3. 适用场景
- 格式化数据(如日期、货币)
- 过滤/排序列表
- 复杂数学计算
- 从大对象中提取小字段
五、异步派生:use_resource(处理网络/IO)
1. 和 use_memo 的区别?
use_memo | use_resource | |
---|---|---|
同步/异步 | 同步 | 异步(async/await) |
是否比较结果 | 是(PartialEq) | 否 |
返回值类型 | T | Resource<T> (包含加载中、错误等状态) |
2. 使用示例
let dog_image = use_resource(move || async move {reqwest::get("https://dog.ceo/api/breeds/image/random").await?.json::<DogApi>().await.map(|d| d.message)
});
- 每次依赖项(比如搜索关键词)变化 → 自动发起新请求。
- 返回的是
Resource<String>
,你可以用.read()
查看状态:match dog_image.read().as_ref() {Some(Ok(url)) => rsx!{ img { src: "{url}" } },Some(Err(e)) => rsx!{ "Error: {e}" },None => rsx!{ "Loading..." }, }
3. 注意事项
- 不要在
use_resource
里直接修改其他 Signal(可能 race condition)。 - 如果请求失败,它会保留错误状态,直到依赖项再次变化。
六、组件也是“派生函数”
在 Dioxus 中,组件就是一个普通函数,但它有特殊能力:
- 它会自动追踪内部使用的 Signal。
- 当这些 Signal 变化 → 组件自动重新运行 → 生成新 UI。
#[component]
fn Counter(count: ReadOnlySignal<i32>) -> Element {// 这里读了 count(),所以组件依赖 countlet double = use_memo(move || count() * 2);rsx! {div { "计数:{count}, 双倍:{double}" }}
}
重要:参数类型用
ReadOnlySignal<T>
而不是T
,才能保持响应性!
七、状态传递的三种方式(详细对比)
方式 1:通过属性(Props)——最清晰
适用场景:父子组件关系明确,状态只在局部共享。
// 父
let count = use_signal(|| 0);
rsx! { Child { count } }// 子
#[component]
fn Child(mut count: Signal<i32>) { ... }
优点:
- 一目了然,谁拥有状态、谁使用状态
- 易于测试和复用
缺点:
- 深层嵌套时,要“透传”很多层(prop drilling)
方式 2:上下文(Context)——跨层级共享
适用场景:多个不相邻组件需要共享状态(如主题、用户信息)。
// 定义上下文类型
struct AppState { count: Signal<i32> }// 父组件提供
use_context_provider(AppState { count: use_signal(|| 0) });// 任意子组件消费
let app = use_context::<AppState>();
优点:
- 避免 prop drilling
- 作用域可控(只在 Provider 子树内有效)
缺点:
- 不如 props 直观
- 过度使用会让数据流变模糊
方式 3:全局变量(Global)——真正的全局
static COUNT: GlobalSignal<i32> = Global::new(|| 0);
优点:
- 任何地方都能用,超级方便
缺点:
- 破坏组件独立性:两个
<Counter/>
会共享同一个值! - 难以测试、难以复用
- 容易造成“隐式依赖”
建议:除非是真正全局的状态(如语言、全局加载提示),否则优先用 Context 或 Props。
八、高级技巧:peek() 和 use_reactive!
1. .peek()
:偷看但不订阅
use_effect(move || {if count() % 2 == 0 {// 偷看 toggle,但不把它加入依赖if *toggle.peek() {log!("偶数且 toggle 开启");}}
});
用途:避免不必要的依赖,防止无限循环。
2. use_reactive!
:让普通值也能被追踪
当你从 props 收到一个普通值 x: i32
,但想在 use_memo
里用它:
let doubled = use_memo(use_reactive!(|(x,)| x * 2)
);
use_reactive!
把普通值包装成“响应式闭包”。- 现在
doubled
会在x
变化时更新。
💡 但更推荐直接用
ReadOnlySignal<i32>
作为 props 类型!
九、最佳实践总结
场景 | 推荐做法 |
---|---|
简单状态 | use_signal |
依赖计算 | use_memo |
网络请求 | use_resource |
父子传状态 | Props(用 Signal 或 ReadOnlySignal ) |
多组件共享 | Context |
真正全局状态 | Global (慎用) |
避免无限循环 | 用 .peek() 或加条件判断 |
保持响应性 | 组件参数用 ReadOnlySignal<T> |
Dioxus 的响应式系统,本质是“自动化的依赖追踪 + 智能更新”。
你只需关心“现在是什么状态,应该显示什么”,剩下的交给框架。