docs/guide/reactivity
Kernkonzepte · Data flow

Reaktivität

Halten Sie Fakten in Signals, leiten Sie mit Memos ab und synchronisieren Sie die Außenwelt mit Effects.

10 Min. Lesezeit

Drei Primitive

01
Signal

Eine beschreibbare Quelle der Wahrheit. Lesen mit get(), schreiben mit set(v); das Lesen registriert eine Abhängigkeit in der aktuellen Berechnung.

02
Memo

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.

03
Effect

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.

counter.zig
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 },
));
DATA FLOW
Ein set läuft die Abhängigkeitskanten entlang – Signal → Memo → textFmt – und nur der abonnierte Textknoten wird render-dirty markiert.

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.

SIGNAL vs DIFF
Der Baum bleibt; was wegfällt, ist das Neuaufbauen eines Kandidatenbaums und die Suche nach Unterschieden bei jeder Änderung.
Die echte Reactive Counter.app: Jedes +1 aktualisiert count und doubled gemeinsam bis 3 / 6, dann setzt Reset beide Subscriber auf null.

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.

handlers.zig
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

APIFunktion
signal.get()Liest und registriert eine Abhängigkeit im aktuellen Memo / Effect
signal.peek()Liest ohne Abonnement – praktisch für Snapshots in Handlern
signal.set(v)Schreibt und benachrichtigt Subscriber
signal.update(fn)Berechnet den nächsten Wert aus dem vorherigen und schreibt ihn
scope.createEffect(ctx, fn)Führt bei Änderung einen Seiteneffekt aus; wird mit seinem Scope freigegeben
effects.zig
// 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.

zenit · DoppellizenzKostenlos für Open-Source-Projekte unter GPL-3.0-only; Closed-Source- oder kommerzielle Produkte benötigen eine kommerzielle Lizenz.Kontakt zum Autor: zongyi.xzy#gmail.com (# durch @ ersetzen)zenit 5f9add5+wip 2026-09-30