响应式信号基础
什么是响应式信号?
响应式信号是 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_name 或 last_name 发生变化时自动更新。computed! 宏接收:
- 依赖信号列表 — 计算所依赖的信号。
- 一个闭包 — 该闭包接收依赖信号的当前值并返回计算结果。
闭包的参数是类型化的,因此编译器确保你使用了正确的类型。返回类型也使用 -> Type 显式指定。
computed! 的底层工作原理
当你创建一个计算信号时:
- 闭包立即被求值以产生初始值。
- 每个依赖信号都被订阅。
- 当任何依赖信号发生变化时,闭包会重新求值。
- 如果新结果与旧结果不同(使用
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);
在这个温度转换器中:
celsius是存储摄氏温度的源信号。fahrenheit是一个计算信号,在celsius变化时自动重新计算。on_input_value处理器在用户输入时更新celsius。- 计算出的
fahrenheit信号自动更新,依赖它的任何 UI 都会重新渲染。
总结
响应式信号是 euv 状态管理的核心:
use_signal创建具有自动依赖追踪的响应式值。.get()和.set()读取和写入信号值。batch()将多个信号更新分组到单次渲染中。computed!创建在依赖变化时自动更新的派生信号。subscribe()注册信号变化的回调。- 信号值必须实现
Clone + PartialEq + 'static。 - Signal 本身实现了
Copy,使其易于在闭包之间共享。
使用这些原语,你可以用最少的样板代码和最佳性能构建复杂的响应式 UI。