响应式信号
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 时,信号状态会在重新渲染之间持久化。后续渲染返回相同的信号句柄,保留其当前值。
Signal 方法汇总
Signal<T> 要求泛型参数 T 满足 Clone + PartialEq + 'static,提供以下方法:
| 方法 | 返回值 | 说明 |
|---|---|---|
Signal::create(value) | Signal<T> | 直接创建信号(不依赖 HookContext,适用于全局信号初始化) |
get(&self) | T | 获取当前值 |
set(&self, value) | () | 设置值并通知监听器 + 调度 DOM 更新(值相同时不触发) |
set_silent(&self, value) | () | 设置值并通知监听器,但不调度 DOM 更新 |
set_untracked(&self, value) | () | 设置值但不通知监听器、不调度 DOM 更新(用于打破循环依赖) |
subscribe(&self, callback) | () | 追加监听器(FnMut() + 'static) |
replace_subscribe(&self, callback) | () | 替换所有监听器为新的一个(框架内部使用) |
clear_listeners(&self) | () | 清除监听器并标记为不活跃(不可逆,框架内部使用) |
读取值
let value: i32 = count.get();写入值
count.set(42);
// 静默写入:更新值并通知监听器,但不触发全局 DOM 更新调度
count.set_silent(42);
// 不追踪写入:设置值但不通知监听器、不调度 DOM 更新
count.set_untracked(42);提示
set 内部做相等性检查,新值与当前值相同时不会触发更新和通知,避免不必要的重新渲染。
注意
set_silent 不会触发 DynamicNode 重新渲染。仅在信号变更不影响 UI 时使用(如内部守卫标志、派生缓存值等)。绝大多数场景请使用 set。
注意
set_untracked 既不通知监听器,也不调度 DOM 更新。适用于打破 watch! 中的循环依赖——当两个信号互相监听时,使用 set_untracked 可以避免无限递归。在非循环依赖场景下,请优先使用 set 或 set_silent。
订阅变化
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 将成为空操作(不更新值、不通知监听器、不调度 DOM 更新)。用于 match 分支切换时清理旧信号,确保过时的 setInterval 闭包引用这些信号时变为无害操作。
注意
clear_listeners 是不可逆操作,调用后信号将永久不活跃。通常由框架内部在 Hook 上下文清理时自动调用,无需手动使用。
use_cleanup
注册一个清理回调,当当前 Hook 上下文被清除时执行(如 match 分支切换时)。适用于清理副作用,如 setInterval、setTimeout 或订阅。
清理回调仅在首次渲染时注册一次,后续重新渲染时为空操作。
let handle: Signal<Option<IntervalHandle>> = use_signal(|| None);
use_cleanup(move || {
if let Some(h) = handle.get() {
h.clear();
}
});提示
use_cleanup 必须在 DynamicNode 的渲染函数内部使用。当 match 分支切换导致 Hook 上下文清理时,所有通过 use_cleanup 注册的回调会按注册顺序执行。对于 keep-alive 模式(CSS display 切换),组件不会被销毁,use_cleanup 不会被调用。
use_window_event
注册 window.addEventListener 事件监听器,使用事件委托机制,在 Hook 上下文清除时自动移除。通过全局窗口事件代理注册表,同一事件名只会在 window 上绑定一次 addEventListener,清理时仅移除处理器条目,共享的 window 监听器保持活跃。
事件监听器仅在首次渲染时注册,后续重新渲染时为空操作。
let route_signal: Signal<String> = use_signal(current_route);
use_window_event("hashchange", move || {
let new_route: String = current_route();
route_signal.set(new_route);
});
use_window_event("resize", move || {
// 处理窗口大小变化
});提示
use_window_event 适用于需要在组件级别监听全局 window 事件的场景,如 hashchange、popstate、resize 等。回调闭包签名为 FnMut() + 'static(不接收 Event 参数)。监听器会在 Hook 上下文清除时自动移除(如 match 分支切换或组件卸载时),无需手动清理。
use_interval
创建一个定时执行的间隔回调,返回 IntervalHandle,在 Hook 上下文清除时自动清除定时器(即组件卸载或 match 分支切换时)。与手动调用 setInterval + Closure::forget() 不同,此 Hook 确保定时器被正确清理,防止内存泄漏和过时回调。
间隔定时器仅在首次渲染时创建,后续重新渲染时返回已有的句柄。
let count: Signal<i32> = use_signal(|| 0);
let count_ref: Signal<i32> = count;
let handle: IntervalHandle = use_interval(1000, move || {
let current: i32 = count_ref.get();
count_ref.set(current + 1);
});IntervalHandle
IntervalHandle 存储浏览器 setInterval 返回的定时器 ID,提供以下方法:
| 方法 | 说明 | |
|---|---|---|
IntervalHandle::new(id) | 创建间隔句柄(框架内部使用) | |
handle.clear(&self) | 取消关联的浏览器间隔定时器,调用后回调不再触发 |
手动提前取消定时器:
let handle: IntervalHandle = use_interval(1000, move || {
// 每秒执行
});
// 提前取消
handle.clear();提示
use_interval 适用于需要周期性执行任务的场景(如倒计时、轮询数据、动画帧等)。通常不需要手动调用 handle.clear(),Hook 上下文清除时会自动清理。但如果需要在定时器运行期间提前取消,可以使用 handle.clear()。
在 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::create
Signal::create 直接创建一个新的响应式信号,不依赖 HookContext。适用于在 DynamicNode 渲染函数外部创建信号(如全局信号初始化):
let count: Signal<i32> = Signal::create(0);
let name: Signal<String> = Signal::create(String::from("euv"));提示
在 DynamicNode 渲染函数内部请使用 use_signal,它会通过 HookContext 管理信号的生命周期,确保重新渲染时信号状态持久化。Signal::create 不会与 HookContext 关联,适用于一次性创建的场景。
Signal 特性
Signal<T> 要求泛型参数 T 满足 Clone + PartialEq + 'static,实现了 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 也看到了变化提示
Signal<T> 不支持直接解引用,必须使用 .get() 和 .set() 方法读写值。
SignalCell
SignalCell<T> 是一个 Sync 包装器,用于在 static 上下文中存储 Signal,实现全局信号访问:
use euv::*;
static GLOBAL_COUNT: SignalCell<i32> = SignalCell::none();
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::none() | 创建一个空的 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 上下文。
批量更新
batch_updates 在闭包执行期间批量处理信号更新,闭包内的 set 调用不会触发 DynamicNode 重新渲染,闭包结束后统一触发一次 DOM 更新:
batch_updates(|| {
// 此闭包内的所有 signal.set() 调用都不会触发 DOM 重新渲染
count.set(1);
name.set("updated".to_string());
});
// 闭包结束后,框架统一调度一次 DOM 更新提示
watch! 宏内部使用 batch_updates 包裹初始执行,防止初始化期间的 .set() 调用触发不必要的 DynamicNode 重渲染。
更新调度机制
信号更新通过 requestAnimationFrame(rAF)批量调度:同一帧内的多次 set 会被合并为一次 DOM 更新,无需手动批量处理。如果 requestAnimationFrame 不可用,则回退到 queueMicrotask。
提示
使用 rAF 调度确保无论同一帧内发生多少次信号更新(如滑块快速拖动),每帧仅执行一次 DOM 更新,避免 CPU 峰值。