第三章:状态管理与 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