响应式信号
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,适用于全局信号初始化) |
Signal::default() | Signal<T> | 使用 T::default() 创建信号(要求 T: Default) |
get(&self) | T | 获取当前值 |
set(&self, value) | () | 设置值并通知监听器 + 调度 DOM 更新(值相同时不触发) |
subscribe(&self, callback) | () | 追加监听器(FnMut() + 'static) |
读取值
let value: i32 = count.get();提示
get 内部实现了精确依赖追踪:如果在 DynamicNode 渲染函数内部调用 get,该信号会自动注册当前动态节点为依赖。当信号变化时,只有依赖该信号的动态节点会被标记为脏并重新渲染,而非广播到所有动态节点。如果信号已被 deactivate,get 仍返回最后存储的值,但不会注册依赖。
写入值
count.set(42);提示
set 内部做相等性检查,新值与当前值相同时不会触发更新和通知,避免不必要的重新渲染。
提示
在 batch 闭包内调用 set 时,DOM 更新会被延迟到闭包结束后统一调度,不会在每次 set 时立即触发 DynamicNode 重新渲染。watch! 宏的初始执行和 computed! 宏的信号更新均在 batch 内进行,确保闭包内的 set 调用不会触发过早的 DOM 更新。
订阅变化
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());
});提示
subscribe 追加一个监听器,信号值变化时会调用所有已注册的监听器。replace_subscribe 和 deactivate 是框架内部使用的方法,不对外暴露。replace_subscribe 用于信号属性绑定时替换监听器,避免在 DynamicNode 重新渲染时产生监听器累积。deactivate 用于 match 分支切换时清理旧信号(清除所有监听器和依赖列表,将信号标记为不活跃),确保过时的异步闭包引用这些信号时变为无害操作。deactivate 不会释放堆内存,因为 Signal<T> 是 Copy 类型(仅存储指针地址),异步回调可能仍持有副本,释放会导致 use-after-free。
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::default
Signal<T> 实现了 Default trait(要求 T: Clone + Default + PartialEq + 'static),调用 Signal::default() 等价于 Signal::create(T::default()):
let count: Signal<i32> = Signal::default(); // 等价于 Signal::create(0)
let name: Signal<String> = Signal::default(); // 等价于 Signal::create(String::new())提示
Signal::default() 主要用于 Props 结构体的 Default 派生,确保 Signal<T> 类型的字段有合理的默认值(创建一个有效的信号,而非空指针)。不要手动构造 inner = 0 的无效信号,调用 .get() 会导致 panic。
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 在闭包执行期间批量处理信号更新,闭包内的 set 调用不会触发 DynamicNode 重新渲染,闭包结束后统一触发一次 DOM 更新:
batch(|| {
count.set(1);
name.set("updated".to_string());
});
// 闭包结束后,框架统一调度一次 DOM 更新提示
batch 适用于同一帧内需要更新多个信号的场景,将多次 DOM 更新合并为一次,避免中间状态的闪烁。
computed! 派生信号
computed! 宏用于从输入信号派生新的响应式信号。当输入信号变化时,计算信号自动重新计算:
let first_name: Signal<String> = use_signal(|| String::from("John"));
let last_name: Signal<String> = use_signal(|| String::from("Doe"));
let full_name: Signal<String> = computed!(first_name, last_name, |first: String, last: String| -> String {
format!("{} {}", first, last)
});提示
computed! 内部通过 use_signal 创建结果信号,使用 set 更新值。更新操作在 batch 闭包内执行,确保不会触发过早的 DOM 调度。详细用法参见 computed! 宏。
更新调度机制
信号更新通过微任务批量调度,采用精确脏标记(precise dirty marking):当信号变化时,只有依赖该信号的动态节点会被标记为脏并重新渲染,而非广播到所有动态节点。set 内部调用 update_and_notify 更新值,然后通过 schedule_update 将依赖该信号的动态节点标记为脏,并调度一次微任务刷新。
调度优先级如下:
queueMicrotask— 首选,最轻量的延迟方式setTimeout(0)— 当queueMicrotask不可用时回退requestAnimationFrame— 当以上两种都不可用时回退
提示
使用微任务调度确保无论同一帧内发生多少次信号更新(如滑块快速拖动),仅执行一次 DOM 更新,避免 CPU 峰值。调度机制会自动选择当前环境可用的最优方案。batch 闭包内 schedule_update 仅标记脏而不调度微任务,闭包结束后由最外层的 set 统一调度。