リアクティビティ
事実は Signal に保持し、派生値は Memo で表し、外部との同期は Effect で行います。
3 つのプリミティブ
書き込み可能な事実の源です。get() で読み、set(v) で書きます。読み取ると現在の計算に依存が登録されます。
他のリアクティブ値から遅延計算されます。入力が変わらなければ前回の結果を再利用し、値が等しければ下流に通知しません。
依存が変わったときに副作用を実行します。ノードの更新、ログ出力、外部システムとの同期などです。
エンドツーエンドのデータフロー
ui.textFmt は渡された Signal / Memo を購読し、それらが変わるとその 1 つのテキストノードだけを再整形します。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 },
));diff ではなく Signal
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);読み取りと購読
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);使い分けの目安
- ✓
1 つの事実に 1 つの Signal。同じ状態を通常のフィールドと Signal の両方に持たないでください。
- ✓
計算できる値は Memo に。派生フィールドを各ハンドラで手作業で同期しないでください。
- ✓
Effect は同期だけ。Effect 自身の依存に無条件で書き戻さないでください。ループになります。
- ✓
寿命は Scope に属します。ページの Scope を破棄すると、その Signal・Memo・Effect も一緒に解放されます。