---
title: "状态与事件 — zenit Zig UI 文档"
description: "把跨帧稳定的状态交给 Cx 保管，用类型安全的 handler 响应用户输入。"
url: https://zenit.z.express/zh/docs/guide/state-events
language: zh-CN
alternate_en: https://zenit.z.express/docs/guide/state-events.md
alternate_es: https://zenit.z.express/es/docs/guide/state-events.md
alternate_ja: https://zenit.z.express/ja/docs/guide/state-events.md
alternate_ko: https://zenit.z.express/ko/docs/guide/state-events.md
alternate_fr: https://zenit.z.express/fr/docs/guide/state-events.md
alternate_de: https://zenit.z.express/de/docs/guide/state-events.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 状态与事件

把跨帧稳定的状态交给 Cx 保管，用类型安全的 handler 响应用户输入。

## 绑定状态

`cx.bindState(T, init)` 在 Cx 的状态仓库里创建一个 `T`，返回跨帧稳定的 `*T`。再用 `cx.on` 把它的方法变成组件可以接收的 `HandlerRef`，不需要手工分配状态 ID。

`counter.zig`

```zig
const Counter = struct {
    allocator: std.mem.Allocator,
    value: u32 = 0,
    label: ?*ui.Node = null,
    buffer: [32]u8 = undefined,

    fn increment(self: *Counter) void {
        self.value += 1;
        const label = self.label orelse return;
        const text = std.fmt.bufPrint(
            &self.buffer,
            "Clicked {d} times",
            .{self.value},
        ) catch return;
        // Copies the text, marks it owned, frees the previous owned copy,
        // and marks the node dirty only if the text actually changed.
        label.setTextContent(self.allocator, text) catch return;
    }
};

const counter = try cx.bindState(Counter, .{ .allocator = cx.allocator });
const on_click = cx.on(Counter, counter, Counter.increment);
```

回调里换文本用 `node.setTextContent(allocator, text)`：它复制内容并标记为 owned，旧的 owned 内容自动释放；只有文本真的变了才标脏，不需要再手动 `markRenderDirty`。结构体因此存一个 `allocator` 字段。

> NOTE
> 
> **匿名，还是可寻址？** `bindState` 每次调用都新建一个条目，ID 按调用顺序匿名分配，只在本次 mount 内有意义。需要在别处或下次 mount 凭 ID 取回同一份状态时，用 `cx.state(T, id, init)` 并自行分配显式 ID。

## 接到组件

组件的回调字段都是 `?HandlerRef`。把上一步得到的 `on_click` 交给 [Button](https://zenit.z.express/zh/components/button)，再把要更新的文本节点记到状态结构体里。

`view.zig`

```zig
const label = try ui.text(cx, "Clicked 0 times", .{
    .color = cx.tokens.color.fg_primary,
});
counter.label = label;

const button = try ui.widgets.Button(.{
    .label = "Increment",
    .variant = .primary,
    .on_click = on_click,
}).mount(scope, cx);

try root.appendChild(cx.allocator, button);
try root.appendChild(cx.allocator, label);
```

需要拿到新值的回调（[Checkbox](https://zenit.z.express/zh/components/checkbox)、Switch 的勾选态，Input、Tabs 的文本或 id）用带值的构造器，例如 `ui.Cx.boolHandlerFrom`。

`prefs.zig`

```zig
const Prefs = struct {
    notify: bool = false,

    fn setNotify(self: *Prefs, checked: bool) void {
        self.notify = checked;
    }
};

const prefs = try cx.bindState(Prefs, .{});

const notify = try ui.widgets.Checkbox(.{
    .label_text = "Notify me",
    .on_change = ui.Cx.boolHandlerFrom(Prefs, prefs, Prefs.setNotify),
}).mount(scope, cx);
```

[Video](https://zenit.z.express/media/stories/checkbox.mp4?v=079ee6a961)

Harness 点击 Checkbox 的真实命中区：勾选视觉、on\_change 回调与状态文本在同一帧链路里更新。

## 一次点击的路径

平台送来的是按下和抬起，不是「点击」。命中测试按 Z 序从上往下找到最上层节点；按下与抬起落在同一元素上时，`EventDispatcher` 合成 `click`，沿 capture → target → bubble 三个阶段分发，在节点上调用 `on_click` 这个 `HandlerRef`，最终进入你绑定的方法。

EVENT ROUTING

HandlerRef 只是「回调函数 + 上下文指针」。cx.on 生成的 callback 把上下文转回 \*Counter 再调用方法；状态本身一直留在 Cx 的状态仓库里。

| 处理结果 | 含义 |
| --- | --- |
| `.ignored` | 未处理，继续传播，允许默认行为 |
| `.handled` | 已处理，继续传播，但阻止默认行为 |
| `.stop` | 已处理并停止传播 |

## Handler 的几种构造

| 构造器 | 方法签名 | 适用 |
| --- | --- | --- |
| `cx.on(State, s, State.m)` | `fn (*State) void` | 按钮点击等「发生了」 |
| `ui.Cx.boolHandlerFrom(…)` | `fn (*State, bool) void` | Checkbox、Switch、Accordion |
| `ui.Cx.strHandlerFrom(…)` | `fn (*State, []const u8) void` | Input、Textarea、Tabs、Radio；切片只在回调期间有效 |
| `ui.clickable(node, handler)` | — | 给任意节点挂 on\_click |

## 底层事件

按钮、输入框等组件已经封装了常见交互。只有实现自定义交互时才需要 `ui.events.Event`：它覆盖鼠标（按下、抬起、click、double\_click、移动、进出、滚轮、捏合、文件拖入）、键盘、`text_input`、`ime_preedit` / `ime_commit` 与焦点。键盘命令优先走 `ui.actions`，焦点移动优先走 `ui.focus`。

| 需求 | 首选 API |
| --- | --- |
| 点击、输入、选择 | `ui.widgets.* callbacks` |
| 悬停、焦点环 | `ui.hooks.useHover / useFocusRing` |
| 快捷键与命令 | `ui.actions` |
| 拖拽、范围悬停 | `ui.interaction.drag / range_hover` |
| 原始键鼠、滚轮、IME | `ui.events.Event` |

## 生命周期与清理

绑定状态归 **Cx** 所有，在 Cx 销毁时释放。如果 `T` 声明了 **pub** 的 `deinit(*T)`，框架会在释放前自动调用它——ArrayList、HashMap、分配的缓冲区都放在这里清理即可。Signal、Memo、Effect 与 `onCleanup` 回调则归 **Scope**，随 `scope.dispose()` 一起释放。

OWNERSHIP

两种生命周期：Cx 级（整个窗口 / 应用）与 Scope 级（一个页面或面板）。

`editor_state.zig`

```zig
const EditorState = struct {
    allocator: std.mem.Allocator,
    lines: std.ArrayList([]u8) = .{},

    // Must be pub: the state store detects it and calls it when the Cx deinits.
    pub fn deinit(self: *EditorState) void {
        for (self.lines.items) |line| self.allocator.free(line);
        self.lines.deinit(self.allocator);
    }
};

// The initial value must not own resources yet; allocate after binding.
const editor = try cx.bindState(EditorState, .{ .allocator = cx.allocator });

// Do NOT also call scope.onCleanup(EditorState, editor, EditorState.deinit):
// the framework already calls deinit, so that would free everything twice.
```

> WARNING
> 
> **不要重复 deinit。** 旧版文档建议对绑定状态再注册 `scope.onCleanup(T, state, T.deinit)`。如果 `deinit` 是 pub，框架已经会调用它，再注册一次会导致重复释放。

`scope.onCleanup` 只用在两种情况：清理函数不是 pub `deinit`，或者资源必须跟随某个 Scope（页面）而不是整个 Cx 释放。

`page_cache.zig`

```zig
// A resource that must follow the page (Scope), not the whole Cx.
const PageCache = struct {
    allocator: std.mem.Allocator,
    hits: std.StringHashMapUnmanaged(u32) = .{},

    // Not named deinit, so the state store will not call it again.
    fn release(self: *PageCache) void {
        self.hits.deinit(self.allocator);
        self.hits = .{};
    }
};

const cache = try cx.bindState(PageCache, .{ .allocator = cx.allocator });
try scope.onCleanup(PageCache, cache, PageCache.release);
```

-   **初值不持有资源。**传给 `bindState` 的初值应是零值或只含指针、分配器这类借用字段；需要分配的东西在拿到 `*T` 之后再建。
    
-   **分配器存成字段。**`deinit(*T)` 只有一个参数，释放时用的分配器要保存在结构体里。
    
-   **不要缓存失效节点。**Node 指针只在所属树和 Scope 的生命周期内有效；页面卸载或重建后，不要再从状态里访问旧节点。
