组件模型
Ratatui Kit 的组件模型分成两层:Element 是每帧重新创建的声明,Component 是运行时复用的持久实例。写应用时你大多在 element! 里声明 UI;框架在 update 阶段把这些声明协调成组件树,并保留 hooks、子树、布局和生命周期。
如果你来自 React,可以把它理解成类似 JSX element 和 component instance 的关系。不同的是这里的输出不是 DOM,而是 ratatui buffer。
一帧发生什么
Section titled “一帧发生什么”一个函数组件每帧都会重新执行:
这里的 element!(Text(...)) 是声明。真正保存 count 的不是这棵临时声明树,而是运行时已经实例化的 Counter 节点和它的 hook 列表。
Element 是声明
Section titled “Element 是声明”Element<T> 可以理解成:
它很轻,每帧都可以重新创建。element! 宏负责把声明式语法变成 element 树:
AnyElement 是类型擦除版本,用来把不同组件类型放进同一个 children 列表。完整宏语法见 声明式语法。
Component 是实例
Section titled “Component 是实例”Component 是持久对象。一个实例持有:
- 组件自身状态。
- hook 列表。
- 子组件树。
- 布局样式。
- 生命周期回调。
函数组件由 #[component] 宏生成 Component 实现。你写的是函数,运行时看到的是一个透明包装组件:每帧 update 时执行函数体,拿到返回的根 element,再协调它的子树。
手写组件直接实现 Component,通常用于桥接原生 ratatui widget 或自定义布局:
完整示例见 原生 Widget 桥接。
运行时协调子节点时,使用 ElementKey + TypeId 匹配上一帧节点:
| 本帧声明 | 上一帧实例 | 结果 |
|---|---|---|
| 同 key、同组件类型 | 命中 | 复用实例,保留 hooks 和子树 |
| 同 key、不同组件类型 | 类型变化 | 重建实例 |
| key 变化 | 身份变化 | 重建实例 |
| 旧 key 消失 | 无声明 | 卸载实例,运行 on_drop |
这就是列表渲染要给稳定 key 的原因:
不要用会增删、重排的数组下标当 key。插入一行后,后面所有下标都会变,旧状态就可能被复用到错误的行上。
静态小数组可以用下标;例如 控制流语法 里的 ROWS 是固定数组,所以 key: index 是稳定的。
控制流属于声明层
Section titled “控制流属于声明层”element! 支持直接写 if、if let、for 和 match:
这些控制流只改变本帧产生的 element 声明。运行时仍然用 key 和类型决定哪些实例能复用。
这也解释了一个重要边界:你可以在 element! 里放条件渲染,但不要把 hook 调用放进条件分支。Hook 顺序属于组件实例,必须每帧稳定;渲染分支属于声明层,可以每帧变化。更多规则见 Hooks。
Props 是传给组件声明的数据。element! 会根据 props 字段构造结构体,并用 Default::default() 补齐未传字段:
自定义 props 用 #[derive(Props)]:
如果 props 里有不能自动 Default 的字段,就手写一个语义中性的 impl Default。这不是装饰性要求:element! 依赖默认值补齐没传的字段。
可选字段直接用 Option<T>。宏会对字段值调用 .into(),所以 top_title: Line::from("title")、top_title: Some(line) 和 top_title: None 都能工作。
Ratatui Kit 的布局字段来自 LayoutStyle:
widthheightflex_directionjustify_contentgapmarginoffset
内置组件会在 props 上暴露这些字段;自定义组件如果也想支持布局属性,需要给 props 加 #[with_layout_style]。
函数组件有一个特殊点:#[component] 生成的是透明布局组件。它不占独立布局节点,而是继承返回的第一个子节点的布局样式。
所以布局属性要写在函数组件返回的根元素上:
不要指望在父级直接给 Panel(width: ...) 生效,除非你把 Panel 写成带 props 的手写组件或显式把布局字段传给内部根元素。
默认组件布局是 flex 切分子区域。需要非 flex 行为时,组件可以重写 calc_children_areas:
ScrollView要把内容区域和滚动 offset 结合起来。Modal要把弹层定位在屏幕中央,并把 input layer 注入子树。- 自定义画布、表格、分栏工作台也可能需要自己的区域计算。
calc_children_areas 必须返回和 children 数量一致的区域列表。少一个区域会导致子节点不被绘制;多一个区域没有对应子节点。调试构建会尽量暴露这类契约问题。