반응성
사실은 Signal에 담고, 파생 값은 Memo로 표현하며, 외부 세계와의 동기화는 Effect로 합니다.
세 가지 프리미티브
쓰기 가능한 사실의 원천입니다. get()으로 읽고 set(v)로 씁니다. 읽으면 현재 계산에 의존성이 기록됩니다.
다른 반응형 값에서 지연 계산됩니다. 입력이 그대로면 이전 결과를 재사용하고, 값이 같으면 하위로 알리지 않습니다.
의존성이 바뀌면 부수 효과를 실행합니다. 노드 갱신, 로그 기록, 외부 시스템 동기화 등입니다.
전체 데이터 흐름
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 },
));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);경험 법칙
- ✓
사실 하나에 Signal 하나. 같은 상태를 일반 필드와 Signal 양쪽에 두지 마십시오.
- ✓
계산 가능한 값은 Memo로. 파생 필드를 핸들러마다 수동으로 동기화하지 마십시오.
- ✓
Effect는 동기화만. Effect 자신의 의존성에 무조건 다시 쓰지 마십시오. 루프가 됩니다.
- ✓
수명은 Scope에 속합니다. 페이지의 Scope가 해제되면 그 안의 Signal, Memo, Effect도 함께 해제됩니다.