响应式系统
用 Signal 保存事实、Memo 表达派生值、Effect 同步外部结果。
预计阅读 10 分钟
三个原语
01
Signal
可写的事实来源。读取用 get(),更新用 set(v);读取会在当前计算中登记依赖。
02
Memo
由其他响应值惰性计算;输入不变时复用上次结果,值相等时不通知下游。
03
Effect
依赖变化时执行副作用,例如更新节点、写日志或同步外部系统。
完整数据流
ui.textFmt 订阅传入的 Signal / Memo,依赖变化时只重排版这一个文本节点。Memo 的依赖通过一个上下文结构体显式传入。
const count = try scope.createSignal(u32, 0);
const doubled = try scope.createMemo(u32, .{ .count = count }, struct {
fn compute(ctx: anytype) u32 {
return ctx.count.get() * 2;
}
}.compute);
try root.appendChild(cx.allocator, try ui.textFmt(
cx,
scope,
"count = {d}, doubled = {d}",
.{ count, doubled },
.{ .color = cx.tokens.color.fg_primary },
));Signal,不是 diff
zenit 保留一棵真实的 UI 树用于布局、命中与绘制,但状态更新不走「重新执行组件 → 生成候选树 → 整树 diff」。读取 Signal 的 Memo / Effect 在订阅时建立依赖;set 只通知这些订阅者,最终把受影响的节点标记为 layout 或 render dirty。
从事件更新 Signal
组件回调是 HandlerRef。用 cx.bindState 保存一个持有 Signal 指针的小结构体,再用 cx.on 把它的方法变成回调。
const Bindings = struct {
count: *ui.Signal(u32),
fn increment(self: *Bindings) void {
self.count.set(self.count.get() + 1);
}
};
const bindings = try cx.bindState(Bindings, .{ .count = count });
const on_click = cx.on(Bindings, bindings, Bindings.increment);
const plus = try ui.widgets.Button(.{
.label = "+1",
.on_click = on_click,
}).mount(scope, cx);读取与订阅
API作用
signal.get()读取并在当前 Memo / Effect 中登记依赖signal.peek()读取但不订阅,适合事件处理器里取快照signal.set(v)写入并通知订阅者signal.update(fn)基于旧值计算新值后写入scope.createEffect(ctx, fn)依赖变化时运行副作用,随 Scope 释放// Effect: sync a value to something outside the reactive graph.
try scope.createEffect(.{ .count = count }, struct {
fn run(ctx: anytype) void {
std.log.info("count is now {d}", .{ctx.count.get()});
}
}.run);
// peek(): read without subscribing (no dependency is recorded).
const snapshot = count.peek();
// update(): read-modify-write in one call.
count.update(struct {
fn inc(v: u32) u32 {
return v + 1;
}
}.inc);选择规则
- ✓
唯一事实用 Signal。不要同时在普通字段和 Signal 里保存同一份状态。
- ✓
可计算的值用 Memo。不要在每个事件处理器里手工同步派生字段。
- ✓
Effect 只做同步。不要在 Effect 中无条件写回自己的依赖,以免形成循环。
- ✓
生命周期归 Scope。页面 Scope 销毁时,其中的 Signal、Memo 与 Effect 一起释放。
TIP