Reactive-Signals-Fundamentals[20260803194249]

0 阅读1分钟

响应式信号基础

项目代码:github.com/euv-dev/euv

什么是响应式信号?

响应式信号是 euv 的状态管理支柱。信号是一个响应式值,在其发生变化时会通知所有订阅者。当你在计算或组件中读取信号的值时,euv 会自动追踪该依赖关系。当信号的值发生变化时,所有依赖的计算和组件都会自动重新求值。

这种模型被称为细粒度响应式,正是它让 euv 如此高效——只有实际依赖于变化值的 UI 部分才会被更新。

使用 use_signal 创建信号

在 euv 中,创建信号的主要方式是使用 use_signal hook。它接收一个初始化函数,该函数产生信号的初始值:

let count: Signal<i32> = use_signal(|| 0);

在这个示例中,count 是一个 Signal<i32>——一个持有 i32 值的信号,初始化为 0。初始化函数 || 0 在信号创建时被调用一次。

你可以创建任何满足 trait 约束的类型的信号:

let name: Signal<String> = use_signal(|| "Alice".to_string());
let is_visible: Signal<bool> = use_signal(|| true);
let items: Signal<Vec<String>> = use_signal(|| vec!["item1".to_string(), "item2".to_string()]);

读取和写入信号值

获取值

要读取信号的当前值,使用 .get() 方法:

let count: Signal<i32> = use_signal(|| 0);
let value: i32 = count.get();

.get() 返回信号当前值的快照。在组件内部调用时,它还会将信号注册为当前渲染上下文的依赖。

设置值

要更新信号的值,使用 .set() 方法:

let count: Signal<i32> = use_signal(|| 0);
count.set(42);

当调用 .set() 时,信号会检查新值是否与旧值实际不同(使用 PartialEq)。如果不同,信号会通知所有订阅者,触发依赖它的任何组件的重新渲染。

信号的 Trait 约束

信号有特定的类型要求。值类型 T 必须实现:

  • Clone — 因为信号在读取时需要生成其值的副本。
  • PartialEq — 因为信号需要比较旧值和新值以避免不必要的更新。
  • 'static — 因为信号可能会在应用的整个生命周期内存储。
// 可以:i32 实现了 Clone、PartialEq 和 'static
let count: Signal<i32> = use_signal(|| 0);

// 可以:String 实现了 Clone、PartialEq 和 'static
let name: Signal<String> = use_signal(|| "default".to_string());

Signal 实现了 Copy

Signal<T> 的一个重要特性是它实现了 Copy。这意味着你可以按值传递信号,而无需显式克隆:

let count: Signal<i32> = use_signal(|| 0);

// count 被复制,而非移动
let reference = count;
// count 和 reference 指向同一个信号
count.set(10);
let value = reference.get(); // value 为 10

这使得在闭包和组件函数之间共享信号变得容易,无需显式的引用计数。

可用的信号方法

Signal 类型提供以下方法:

方法描述
create(initializer)使用给定的初始化函数创建新信号
default()使用类型 T 的默认值创建信号
get()返回信号的当前值
set(value)设置信号的新值
subscribe(callback)订阅一个在信号变化时调用的回调

使用 subscribe

subscribe 方法允许你注册一个在信号值发生变化时触发的回调:

let count: Signal<i32> = use_signal(|| 0);

count.subscribe(move |value: i32| {
    // 每次 count 变化时都会执行
});

这对于需要在状态变化时发生的副作用很有用,例如日志记录、分析或其他信号的触发。

批量更新

当你需要同时更新多个信号时,可以使用 batch 函数将更新分组在一起。这可以防止不必要的中间渲染:

let count: Signal<i32> = use_signal(|| 0);
let name: Signal<String> = use_signal(|| "default".to_string());

batch(|| {
    count.set(1);
    name.set("updated".to_string());
});

如果没有 batch,每次 .set() 调用都会触发单独的渲染。使用 batch 后,更新会被延迟并一起应用,从而产生单次渲染。当你有多个信号需要在响应用户操作时同时更新,这一点特别重要。

使用 computed! 创建派生信号

派生信号(也称为计算信号)是一种值从其他信号计算得出的信号。你可以使用 computed! 宏来创建:

let first_name: Signal<String> = use_signal(|| "John".to_string());
let last_name: Signal<String> = use_signal(|| "Doe".to_string());

let full_name: Signal<String> = computed!(first_name, last_name,
    |first: String, last: String| -> String {
        format!("{} {}", first, last)
    }
);

在这个示例中,full_name 会在 first_namelast_name 发生变化时自动更新。computed! 宏接收:

  1. 依赖信号列表 — 计算所依赖的信号。
  2. 一个闭包 — 该闭包接收依赖信号的当前值并返回计算结果。

闭包的参数是类型化的,因此编译器确保你使用了正确的类型。返回类型也使用 -> Type 显式指定。

computed! 的底层工作原理

当你创建一个计算信号时:

  1. 闭包立即被求值以产生初始值。
  2. 每个依赖信号都被订阅。
  3. 当任何依赖信号发生变化时,闭包会重新求值。
  4. 如果新结果与旧结果不同(使用 PartialEq),计算信号会通知其自身的订阅者。

这创建了一个响应式链:当源信号发生变化时,依赖它的计算信号会自动更新,这又会触发依赖于它们的任何组件或其他计算信号的更新。

其他实用 Hook

use_cleanup

use_cleanup hook 允许你注册一个在组件卸载时运行的清理函数:

use_cleanup(move || {
    // 清理代码:移除监听器、清除资源等
});

use_window_event

use_window_event hook 将事件监听器附加到 window 对象:

use_window_event("hashchange", move || {
    // 处理 hash 变化
});

use_interval

use_interval hook 设置一个循环定时器:

let handle: IntervalHandle = use_interval(1000, move || {
    // 这个回调每 1000 毫秒运行一次
});
handle.clear();

use_interval 返回的 IntervalHandle 有一个 .clear() 方法来停止定时器。

综合示例

下面是一个更完整的示例,展示了信号、计算值和批量更新:

use euv::*;

fn app() -> VirtualNode {
    let celsius: Signal<f64> = use_signal(|| 0.0);

    let fahrenheit: Signal<f64> = computed!(celsius,
        |c: f64| -> f64 {
            c * 9.0 / 5.0 + 32.0
        }
    );

    html! {
        div {
            h1 { "Temperature Converter" }
            input {
                oninput: on_input_value(celsius)
            }
            p { "Fahrenheit" }
        }
    }
}

mount("#app", app);

在这个温度转换器中:

  1. celsius 是存储摄氏温度的源信号。
  2. fahrenheit 是一个计算信号,在 celsius 变化时自动重新计算。
  3. on_input_value 处理器在用户输入时更新 celsius
  4. 计算出的 fahrenheit 信号自动更新,依赖它的任何 UI 都会重新渲染。

总结

响应式信号是 euv 状态管理的核心:

  • use_signal 创建具有自动依赖追踪的响应式值。
  • .get().set() 读取和写入信号值。
  • batch() 将多个信号更新分组到单次渲染中。
  • computed! 创建在依赖变化时自动更新的派生信号。
  • subscribe() 注册信号变化的回调。
  • 信号值必须实现 Clone + PartialEq + 'static
  • Signal 本身实现了 Copy,使其易于在闭包之间共享。

使用这些原语,你可以用最少的样板代码和最佳性能构建复杂的响应式 UI。


项目代码:github.com/euv-dev/euv