Reaktivität
Halten Sie Fakten in Signals, leiten Sie mit Memos ab und synchronisieren Sie die Außenwelt mit Effects.
Drei Primitive
Eine beschreibbare Quelle der Wahrheit. Lesen mit get(), schreiben mit set(v); das Lesen registriert eine Abhängigkeit in der aktuellen Berechnung.
Wird lazy aus anderen reaktiven Werten berechnet; nutzt das letzte Ergebnis wieder, solange die Eingaben gleich bleiben, und benachrichtigt niemanden, wenn der Wert gleich ist.
Führt einen Seiteneffekt aus, wenn sich seine Abhängigkeiten ändern – einen Knoten aktualisieren, loggen, ein externes System synchronisieren.
Datenfluss von Anfang bis Ende
ui.textFmt abonniert die übergebenen Signals / Memos und formatiert bei Änderungen nur diesen einen Textknoten neu. Ein Memo erhält seine Abhängigkeiten über ein explizites Kontext-Struct.
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 },
));Signals statt Diffing
zenit behält einen echten UI-Baum für Layout, Hit-Testing und Zeichnen, aber Updates laufen nicht über „Komponenten neu ausführen → Kandidatenbaum bauen → alles diffen“. Memos und Effects, die ein Signal lesen, registrieren eine Abhängigkeit; set benachrichtigt genau diese Subscriber, die die betroffenen Knoten als layout- oder render-dirty markieren.
Aus Events aktualisieren
Komponenten-Callbacks sind HandlerRefs. Halten Sie mit cx.bindState ein kleines Struct mit dem Signal-Zeiger und machen Sie mit cx.on eine seiner Methoden zum Callback.
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);Lesen und abonnieren
signal.get()Liest und registriert eine Abhängigkeit im aktuellen Memo / Effectsignal.peek()Liest ohne Abonnement – praktisch für Snapshots in Handlernsignal.set(v)Schreibt und benachrichtigt Subscribersignal.update(fn)Berechnet den nächsten Wert aus dem vorherigen und schreibt ihnscope.createEffect(ctx, fn)Führt bei Änderung einen Seiteneffekt aus; wird mit seinem Scope freigegeben// 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);Faustregeln
- ✓
Ein Fakt, ein Signal. Halten Sie denselben State nicht zugleich in einem normalen Feld und in einem Signal.
- ✓
Ableitbare Werte sind Memos. Synchronisieren Sie abgeleitete Felder nicht in jedem Handler von Hand.
- ✓
Effects synchronisieren nur. Schreiben Sie nie bedingungslos in eine Abhängigkeit des Effects selbst zurück – das ist eine Schleife.
- ✓
Lebensdauern gehören zu einem Scope. Wird der Scope einer Seite freigegeben, gehen seine Signals, Memos und Effects mit.