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_memouse_resource
同步/异步同步异步(async/await)
是否比较结果是(PartialEq)
返回值类型TResource<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(),所以组件依赖 count
    let 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(用 SignalReadOnlySignal
多组件共享Context
真正全局状态Global(慎用)
避免无限循环.peek() 或加条件判断
保持响应性组件参数用 ReadOnlySignal<T>

Dioxus 的响应式系统,本质是“自动化的依赖追踪 + 智能更新”
你只需关心“现在是什么状态,应该显示什么”,剩下的交给框架。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编码浪子

您的支持将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值