响应式信号
use_signal
创建响应式信号:
use euv::*;
let count: Signal<i32> = use_signal(|| 0);
let name: Signal<String> = use_signal(|| String::from("euv"));
let visible: Signal<bool> = use_signal(|| true);提示
在 DynamicNode 渲染函数内部调用 use_signal 时,信号状态会在重新渲染之间持久化。后续渲染返回相同的信号句柄,保留其当前值。
读取值
let value: i32 = count.get();
// 尝试读取(内部借用失败时仍可能 panic)
let maybe_value: Option<i32> = count.try_get();写入值
count.set(42);
// 值相同时不更新,返回 false
let updated: bool = count.try_set(42);
// 静默写入:更新值并通知监听器,但不触发全局 DOM 更新调度
count.set_silent(42);提示
set 内部做相等性检查,新值与当前值相同时不会触发更新和通知,避免不必要的重新渲染。
注意
set_silent 不会调用 schedule_signal_update(),因此不会触发 DynamicNode 重新渲染。仅在信号变更不影响 UI 时使用(如内部守卫标志、派生缓存值等)。绝大多数场景请使用 set。
注意
try_get 内部仍使用 RefCell::borrow(),在借用冲突时会 panic,并非安全替代方案。其返回 Option<T> 仅为与 try_set 对称设计的 API。
订阅变化
subscribe — 追加监听器
let count_for_sub: Signal<i32> = count;
count.subscribe(move || {
let new_value: i32 = count_for_sub.get();
web_sys::console::log_1(&format!("Count changed to: {}", new_value).into());
});replace_subscribe — 替换监听器
清除所有现有监听器,然后添加新的监听器。确保每个信号最多只有一个活跃监听器,防止监听器在重新渲染时累积:
let count_for_sub: Signal<i32> = count;
count.replace_subscribe(move || {
let new_value: i32 = count_for_sub.get();
// 处理变化
});提示
框架内部在信号属性绑定时使用 replace_subscribe,避免在 DynamicNode 重新渲染时产生监听器累积。
clear_listeners — 清除监听器并标记为不活跃
count.clear_listeners();清除所有监听器并将信号标记为不活跃。之后对该信号调用 set 或 try_set 将成为空操作(不更新值、不通知监听器、不调度 DOM 更新)。用于 match 分支切换时清理旧信号,确保过时的 setInterval 闭包引用这些信号时变为无害操作。
注意
clear_listeners 是不可逆操作,调用后信号将永久不活跃。通常由框架内部在 Hook 上下文清理时自动调用,无需手动使用。
在 HTML 宏中使用
fn counter() -> VirtualNode {
let count: Signal<i32> = use_signal(|| 0);
let count_updater: Signal<i32> = count;
html! {
div {
p {
"Count: "
count
}
button {
onclick: move |_event: Event| {
let current: i32 = count_updater.get();
count_updater.set(current + 1);
}
"Increment"
}
}
}
}注意
Signal 不支持直接解引用,必须使用 .get() 和 .set() 方法,否则会 panic。
Signal 特性
Signal<T> 实现了 Copy 和 Clone,所有副本共享相同的内部状态。这是因为 Signal 本质上是一个原始指针,复制只是比特位的拷贝。
let signal_a: Signal<i32> = use_signal(|| 0);
let signal_b: Signal<i32> = signal_a; // Copy,共享状态
signal_a.set(42);
assert_eq!(signal_b.get(), 42); // signal_b 也看到了变化SignalCell
SignalCell<T> 是一个 Sync 包装器,用于在 static 上下文中存储 Signal,实现全局信号访问:
use euv::*;
static GLOBAL_COUNT: SignalCell<i32> = SignalCell::empty();
fn init_global_count() {
let count: Signal<i32> = use_signal(|| 0);
GLOBAL_COUNT.set(count);
}
fn get_global_count() -> Signal<i32> {
GLOBAL_COUNT.get()
}SignalCell 提供的方法:
| 方法 | 说明 |
|---|---|
SignalCell::empty() | 创建一个空的 SignalCell,适合在 static 上下文中使用 |
cell.set(signal) | 存储 Signal 到 cell 中,重复调用会 panic |
cell.get() | 获取存储的 Signal,未初始化时调用会 panic |
注意
SignalCell 仅适用于单线程 WASM 环境。虽然它实现了 Sync 以允许作为 static 变量使用,但在多线程环境中并发访问会导致未定义行为。
提示
SignalCell 适用于需要在多个函数间共享全局信号的场景,如全局状态管理。在组件内部使用 use_signal 即可,无需 SignalCell。
HookContext
HookContext 管理动态节点的 Hook 状态,在渲染周期之间持久化 use_signal 等钩子状态。框架内部自动为每个 DynamicNode 创建和管理 HookContext。
提示
通常不需要手动使用 HookContext,html! 宏自动为 DynamicNode 和条件渲染创建和管理 Hook 上下文。
抑制更新调度
with_suppressed_updates 在闭包执行期间抑制信号更新调度,闭包内的 set 调用不会触发 DynamicNode 重新渲染:
with_suppressed_updates(|| {
// 此闭包内的所有 signal.set() 调用都不会触发 DOM 重新渲染
count.set(1);
name.set("updated".to_string());
});提示
watch! 宏内部使用 with_suppressed_updates 包裹初始执行,防止初始化期间的 .set() 调用触发不必要的 DynamicNode 重渲染。
更新调度机制
信号更新通过微任务(microtask)批量调度:同一同步代码块中的多次 set 会被合并为一次 DOM 更新,无需手动批量处理。set 调用后,框架通过 schedule_signal_update() 调度一个微任务,在微任务中派发 __euv_signal_update__ 事件,触发所有订阅的 DynamicNode 重新渲染。