上一节我们介绍了 Ratatui Kit 的自动布局和递归渲染机制。本节将进一步探讨组件的动态更新原理,包括节点唯一标识、组件实例化简化、更新辅助工具的设计,以及高效的组件复用与最小化重建流程。
在组件化 UI 框架中,组件树的每个节点都需要一个唯一的 key。这样,组件树结构发生变化时,框架才能准确地定位、复用和管理每个组件实例。
首先安装 any_key
依赖:
cargo add any_key
核心实现如下:
use any_key::AnyHash;
use std::{fmt::Debug, hash::Hash, sync::Arc};
/// ElementKey:唯一标识组件树节点,支持任意可哈希类型
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct ElementKey(Arc<Box<dyn AnyHash + Send + Sync>>);
impl ElementKey {
/// 创建新的 ElementKey
pub fn new<T>(value: T) -> Self
where
T: Debug + Send + Sync + AnyHash,
{
Self(Arc::new(Box::new(value)))
}
}
在组件实例结构体中添加 key 字段,实例化时传入:
pub struct InstantiatedComponent {
key: ElementKey,
component: Box<dyn AnyComponent>,
children: Components,
helper: Box<dyn ComponentHelperExt>,
layout_style: LayoutStyle,
}
impl InstantiatedComponent {
pub fn new(key: ElementKey, mut props: AnyProps, helper: Box<dyn ComponentHelperExt>) -> Self {
let component = helper.new_component(props.borrow());
Self {
key,
component,
helper,
children: Components::default(),
layout_style: LayoutStyle::default(),
hooks: Default::default(),
}
}
// ...
}
传统的组件实例化方式较为繁琐,需要手动传递多项参数。为提升开发体验,我们引入了 Element
结构体,将 key、props 和类型信息封装在一起,实现更简洁的声明方式。
use crate::props::AnyProps;
pub mod key;
use key::ElementKey;
pub struct Element<'a> {
pub key: ElementKey,
pub props: AnyProps<'a>,
}
为保证类型安全和泛型能力,引入 ElementType
trait:
pub mod key;
use crate::component::Component;
use key::ElementKey;
/// ElementType trait:为每种组件类型定义 Props 类型,便于泛型处理
pub trait ElementType {
type Props<'a>
where
Self: 'a;
}
impl<C> ElementType for C
where
C: Component,
{
type Props<'a> = C::Props<'a>;
}
pub struct Element<'a, T: ElementType + 'a> {
pub key: ElementKey,
pub props: T::Props<'a>,
}
类型擦除层 AnyElement
用于统一管理不同类型组件:
use crate::{
component::{
Component,
component_helper::{ComponentHelper, ComponentHelperExt},
},
element::{Element, key::ElementKey},
props::AnyProps,
};
pub struct AnyElement<'a> {
key: ElementKey,
props: AnyProps<'a>,
helper: Box<dyn ComponentHelperExt>,
}
impl<'a, T> From<Element<'a, T>> for AnyElement<'a>
where
T: Component,
{
fn from(value: Element<'a, T>) -> Self {
Self {
key: value.key,
props: AnyProps::owned(value.props),
helper: ComponentHelper::<T>::boxed(),
}
}
}
impl<'a, 'b: 'a, T> From<&'a mut Element<'b, T>> for AnyElement<'a>
where
T: Component,
{
fn from(value: &'a mut Element<'b, T>) -> Self {
Self {
key: value.key.clone(),
props: AnyProps::borrowed(&mut value.props),
helper: ComponentHelper::<T>::boxed(),
}
}
}
impl<'a, 'b: 'a> From<&'a mut AnyElement<'b>> for AnyElement<'b> {
fn from(value: &'a mut AnyElement<'b>) -> Self {
Self {
key: value.key.clone(),
props: value.props.borrow(),
helper: value.helper.copy(),
}
}
}
组件类型与实例分离,trait 无法直接访问实例相关信息。为此,我们设计了 ComponentUpdater
,专门辅助组件的更新和属性变更。
use crate::{
component::instantiated_component::Components, element::key::ElementKey,
render::layout_style::LayoutStyle,
};
pub struct ComponentUpdater<'a> {
key: ElementKey,
components: &'a mut Components,
layout_style: &'a mut LayoutStyle,
}
impl<'a> ComponentUpdater<'a> {
pub fn new(
key: ElementKey,
components: &'a mut Components,
layout_style: &'a mut LayoutStyle,
) -> Self {
Self {
key,
components,
layout_style,
}
}
pub fn set_layout_style(&mut self, layout_style: LayoutStyle) {
*self.layout_style = layout_style;
}
}
在 Component
trait 中添加 update
方法,参数为 ComponentUpdater
,以支持自定义组件的更新逻辑。
pub trait Component: Any {
type Props<'a>
where
Self: 'a;
// ...
fn update(&mut self, props: &mut Self::Props<'_>, updater: &mut ComponentUpdater<'_>) {}
}
类型擦除层也需支持 update:
pub trait AnyComponent {
// ...
fn update(&mut self, props: AnyProps, updater: &mut ComponentUpdater<'_>);
}
impl<T> AnyComponent for T
where
T: Component,
{
// ...
fn update(&mut self, mut props: AnyProps, updater: &mut ComponentUpdater<'_>) {
Component::update(self, unsafe { props.downcast_mut_unchecked() }, updater);
}
}
ComponentHelperExt
trait 增加统一的更新方法:
use std::any::TypeId;
use crate::{
component::{AnyComponent, Component},
props::AnyProps,
render::updater::ComponentUpdater,
};
pub trait ComponentHelperExt {
// ...
fn update_component(
&self,
component: &mut Box<dyn AnyComponent>,
props: AnyProps,
updater: &mut ComponentUpdater,
);
fn component_type_id(&self) -> TypeId;
}
// ...
impl<T> ComponentHelperExt for ComponentHelper<T>
where
T: Component,
{
// ...
fn update_component(
&self,
component: &mut Box<dyn AnyComponent>,
props: AnyProps,
updater: &mut ComponentUpdater,
) {
component.update(props, updater);
}
fn component_type_id(&self) -> TypeId {
TypeId::of::<T>()
}
}
组件实例实现 update 方法,利用 updater 完成更新:
impl InstantiatedComponent {
// ...
fn update(&mut self, props: AnyProps) {
let mut updater =
ComponentUpdater::new(self.key.clone(), &mut self.children, &mut self.layout_style);
self.helper
.update_component(&mut self.component, props, &mut updater);
}
// 返回组件
fn component(&self) -> &dyn AnyComponent {
&*self.component
}
}
为支持高效查找和复用,Components
由数组改为映射,key 为 ElementKey
。
use std::{
collections::{HashMap, VecDeque},
hash::Hash,
};
/// AppendOnlyMultimap 是一个多值映射,允许向末尾追加值。
/// 它的主要特点是只支持追加操作,不支持删除操作。
pub(crate) struct AppendOnlyMultimap<K, V> {
items: Vec<Option<V>>, // 存储所有值的容器
m: HashMap<K, VecDeque<usize>>, // 键到索引的映射
}
impl<K, V> Default for AppendOnlyMultimap<K, V> {
fn default() -> Self {
Self {
items: Vec::new(),
m: HashMap::new(),
}
}
}
impl<K, V> AppendOnlyMultimap<K, V>
where
K: Eq + Hash,
{
/// 向 multimap 末尾追加一个值,关联到指定的键。
pub fn push_back(&mut self, key: K, value: V) {
let index = self.items.len();
self.items.push(Some(value));
self.m.entry(key).or_default().push_back(index);
}
}
/// RemoveOnlyMultimap 是一个多值映射,允许从前面移除值。
/// 它的主要特点是只支持删除操作,不支持追加操作。
pub struct RemoveOnlyMultimap<K, V> {
items: Vec<Option<V>>, // 存储所有值的容器
m: HashMap<K, VecDeque<usize>>, // 键到索引的映射
}
impl<K, V> Default for RemoveOnlyMultimap<K, V> {
fn default() -> Self {
Self {
items: Vec::new(),
m: HashMap::new(),
}
}
}
impl<K, V> From<AppendOnlyMultimap<K, V>> for RemoveOnlyMultimap<K, V>
where
K: Eq + Hash,
{
fn from(value: AppendOnlyMultimap<K, V>) -> Self {
Self {
items: value.items,
m: value.m,
}
}
}
impl<K, V> RemoveOnlyMultimap<K, V>
where
K: Eq + Hash,
{
/// 从 multimap 中移除与指定键关联的第一个值。
pub fn pop_front(&mut self, key: &K) -> Option<V> {
let index = self.m.get_mut(key)?.pop_front()?;
self.items[index].take()
}
/// 遍历 multimap 中的所有值。
pub fn iter(&self) -> impl Iterator<Item = &V> {
self.items.iter().filter_map(|item| item.as_ref())
}
/// 遍历 multimap 中的所有值(可变引用)。
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut V> {
self.items.iter_mut().filter_map(|item| item.as_mut())
}
}
组件集合结构体定义:
pub struct Components {
pub components: RemoveOnlyMultimap<ElementKey, InstantiatedComponent>,
}
组件更新的核心流程如下:
graph TD A[接收新children] --> B[创建used_components] B --> C{遍历新children} C -->|匹配key和类型| D[复用旧组件] C -->|不匹配| E[新建组件] D --> F[更新props] E --> F[更新props] F --> G[收集used_components] G --> H[替换旧components]
实现代码:
impl<'a> ComponentUpdater<'a> {
/// 根据传入的 children 列表,更新当前组件的所有子组件。
///
/// 算法说明:
/// 1. 遍历新的 children(每个 AnyElement),尝试用 key 从旧组件映射中取出对应的 InstantiatedComponent。
/// 2. 如果 key 匹配且类型一致,则复用旧组件实例,否则新建一个组件实例。
/// 3. 对每个组件实例调用 update,传入新的 props。
/// 4. 将本轮用到的组件按顺序插入新的 multimap,最后整体替换原有的 components。
///
/// 这样可以保证:
/// - 组件 key 不变且类型一致时,组件实例被复用,保留内部状态。
/// - key 变更或类型不一致时,自动销毁旧实例并新建,保证类型安全。
/// - 未被复用的旧组件会被丢弃,实现“最小化重建”。
pub fn update_children<T>(&mut self, children: T)
where
T: IntoIterator<Item = AnyElement<'a>>,
{
// 新建一个 multimap,用于存放本轮更新后实际用到的组件实例
let mut used_compoent = AppendOnlyMultimap::default();
// 遍历新的 children 列表
for mut child in children {
// 尝试用 key 从旧组件集合中取出一个实例
let mut component = match self.components.pop_front(&child.key) {
// 如果 key 匹配且类型一致,则复用旧组件实例
Some(component)
if component.component().type_id() == child.helper.component_type_id() =>
{
component
}
// 否则新建一个组件实例
_ => {
let h = child.helper.copy();
InstantiatedComponent::new(child.key.clone(), child.props.borrow(), h)
}
};
// 用新的 props 更新组件实例
component.update(child.props.borrow());
// 将本轮用到的组件实例插入 multimap
used_compoent.push_back(child.key.clone(), component);
}
// 用新的 multimap 替换原有的 components,实现“最小化重建”
self.components.components = used_compoent.into();
}
}
说明: 为了获取 AnyComponent 的 type_id,需要为
AnyComponent
trait 添加Any
约束:
pub trait AnyComponent: Any {
// ...
}
通过上述设计,组件的状态复用、最小化重建都能高效实现,为终端 UI 框架的动态能力提供了坚实基础。
在实际开发中,我们希望直接用 Element 这样的声明式结构描述 UI,然后一行代码就能启动渲染主循环,而不是手动实例化组件。为此,可以给 Element 扩展一个 trait(ElementExt),让它支持统一的“渲染主循环”接口。这样无论是 Element 还是 AnyElement,都能直接调用 render_loop,自动完成组件树的构建和渲染。
下面是 ElementExt trait 的定义和实现:
// Element 扩展 trait 及相关工具,便于统一操作不同类型的 Element
use super::ElementKey;
use crate::{component::component_helper::ComponentHelperExt, props::AnyProps};
use std::io;
/// 私有模块,用于实现 trait 封装,防止外部实现 ElementExt
mod private {
use crate::{
component::Component,
element::{AnyElement, Element},
};
/// Sealed trait,防止外部实现 ElementExt
pub trait Sealed {}
// 为 AnyElement 及其可变引用实现 Sealed
impl<'a> Sealed for AnyElement<'a> {}
impl<'a> Sealed for &mut AnyElement<'a> {}
// 为泛型 Element 及其可变引用实现 Sealed
impl<'a, T> Sealed for Element<'a, T> where T: Component {}
impl<'a, T> Sealed for &mut Element<'a, T> where T: Component {}
}
/// ElementExt trait:为 Element/AnyElement 提供统一的扩展方法
pub trait ElementExt: private::Sealed + Sized {
/// 获取节点唯一 key
fn key(&self) -> &ElementKey;
/// 获取可变 props(类型擦除)
fn props_mut(&mut self) -> AnyProps;
/// 获取组件 helper
fn helper(&self) -> Box<dyn ComponentHelperExt>;
/// 启动渲染主循环
fn render_loop(&mut self) -> impl Future<Output = io::Result<()>>;
}
有了 ElementExt 之后,更新子组件时也可以用统一的 trait 处理不同类型的 Element,代码更简洁:
pub fn update_children<T, E>(&mut self, children: T)
where
T: IntoIterator<Item = E>,
E: ElementExt,
{
// 新建一个 multimap,用于存放本轮更新后实际用到的组件实例
let mut used_compoent = AppendOnlyMultimap::default();
// 遍历新的 children 列表
for mut child in children {
// 尝试用 key 从旧组件集合中取出一个实例
let mut component = match self.components.pop_front(&child.key()) {
// 如果 key 匹配且类型一致,则复用旧组件实例
Some(component)
if component.component().type_id() == child.helper().component_type_id() =>
{
component
}
// 否则新建一个组件实例
_ => {
let h = child.helper().copy();
InstantiatedComponent::new(child.key().clone(), child.props_mut(), h)
}
};
// 用新的 props 更新组件实例
component.update(child.props_mut());
// 将本轮用到的组件实例插入 multimap
used_compoent.push_back(child.key().clone(), component);
}
// 用新的 multimap 替换原有的 components,实现“最小化重建”
self.components.components = used_compoent.into();
}
Tree 结构体新增 props 属性,并在渲染前自动调用 update 方法,保证每次渲染前都能同步最新的 props。
pub struct Tree<'a> {
root_component: InstantiatedComponent,
props: AnyProps<'a>,
}
impl<'a> Tree<'a> {
pub fn new(mut props: AnyProps<'a>, helper: Box<dyn ComponentHelperExt>) -> Self {
Self {
root_component: InstantiatedComponent::new(
ElementKey::new("__root__"),
props.borrow(),
helper,
),
props,
}
}
pub fn render(&mut self, terminal: &mut ratatui::DefaultTerminal) -> io::Result<()> {
self.root_component.update(self.props.borrow());
terminal.draw(|frame| {
let area = frame.area();
let mut drawer = ComponentDrawer::new(frame, area);
self.root_component.draw(&mut drawer);
})?;
Ok(())
}
pub async fn render_loop(&mut self) -> io::Result<()> {
let mut terminal = ratatui::init();
let mut event_stream = EventStream::new();
loop {
self.render(&mut terminal)?;
if let Some(Ok(event)) = event_stream.next().await {
if let Event::Key(key) = event {
match key.code {
KeyCode::Char('q') => break,
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
break;
}
_ => {}
}
}
}
}
ratatui::restore();
Ok(())
}
}
我们还可以在 tree.rs 里加一个通用的渲染函数,方便所有实现了 ElementExt 的类型直接启动主循环:
pub(crate) async fn render_loop<E: ElementExt>(mut element: E) -> io::Result<()> {
let helper = element.helper();
let mut tree = Tree::new(element.props_mut(), helper);
tree.render_loop().await?;
Ok(())
}
最后,为 Element 及其可变引用实现 ElementExt trait,这样就能直接用 Element 启动渲染循环了:
impl<'a, T> ElementExt for Element<'a, T>
where
T: Component,
{
fn key(&self) -> &ElementKey {
&self.key
}
fn helper(&self) -> Box<dyn ComponentHelperExt> {
ComponentHelper::<T>::boxed()
}
fn props_mut(&mut self) -> AnyProps {
AnyProps::borrowed(&mut self.props)
}
async fn render_loop(&mut self) -> io::Result<()> {
render_loop(self).await?;
Ok(())
}
}
impl<'a, T> ElementExt for &mut Element<'a, T>
where
T: Component,
{
fn key(&self) -> &ElementKey {
&self.key
}
fn helper(&self) -> Box<dyn ComponentHelperExt> {
ComponentHelper::<T>::boxed()
}
fn props_mut(&mut self) -> AnyProps {
AnyProps::borrowed(&mut self.props)
}
async fn render_loop(&mut self) -> io::Result<()> {
render_loop(&mut **self).await?;
Ok(())
}
}
同理,AnyElement 也可以实现 ElementExt,具体实现和 Element 类似,这里就不赘述了。
通过这种方式,我们把组件实例的中间层封装了起来。开发者只需要声明 UI 结构,直接调用 render_loop 就能启动完整的渲染和事件循环,极大提升了易用性和扩展性。
添加View
组件:
#[derive(Default)]
pub struct ViewProps<'a> {
/// 主轴方向(横向/纵向)
pub flex_direction: Direction,
/// 主轴对齐方式(如 Start, End, Center, SpaceBetween 等)
pub justify_content: Flex,
/// 子项间距
pub gap: i32,
/// 外边距
pub margin: Margin,
/// 偏移量
pub offset: Offset,
/// 宽度约束
pub width: Constraint,
/// 高度约束
pub height: Constraint,
pub children: Vec<AnyElement<'a>>,
}
pub struct View;
impl Component for View {
type Props<'a> = ViewProps<'a>;
fn new(_props: &Self::Props<'_>) -> Self {
Self
}
fn update(
&mut self,
props: &mut Self::Props<'_>,
updater: &mut ratatui_kit_principle::render::updater::ComponentUpdater<'_>,
) {
updater.set_layout_style(LayoutStyle {
flex_direction: props.flex_direction,
justify_content: props.justify_content,
gap: props.gap,
margin: props.margin,
offset: props.offset,
width: props.width,
height: props.height,
});
updater.update_children(props.children.iter_mut());
}
}
修改main函数
#[tokio::main]
async fn main() -> io::Result<()> {
let count = 0;
let counter_text = format!("Count: {}", count);
let mut element = Element::<View> {
key: ElementKey::new("root"),
props: ViewProps {
children: vec![
Element::<View> {
key: ElementKey::new("header"),
props: ViewProps {
children: vec![
Element::<Text> {
key: ElementKey::new("title"),
props: TextProps {
text: "Welcome to the Counter App",
style: Style::default().bold().light_blue(),
alignment: ratatui::layout::Alignment::Center,
},
}
.into(),
],
height: Constraint::Length(1),
..Default::default()
},
}
.into(),
Element::<View> {
key: ElementKey::new("body"),
props: ViewProps {
children: vec![
Element::<Text> {
key: ElementKey::new("number"),
props: TextProps {
text: counter_text.as_str(),
style: Style::default().light_green(),
alignment: ratatui::layout::Alignment::Center,
},
}
.into(),
],
height: Constraint::Fill(1),
..Default::default()
},
}
.into(),
Element::<View> {
key: ElementKey::new("footer"),
props: ViewProps {
children: vec![
Element::<Text> {
key: ElementKey::new("info"),
props: TextProps {
text: "Press q or Ctrl+C to quit, + to increase, - to decrease",
style: Style::default().yellow(),
alignment: ratatui::layout::Alignment::Center,
},
}
.into(),
],
height: Constraint::Length(1),
..Default::default()
},
}
.into(),
],
flex_direction: Direction::Vertical,
gap: 3,
..Default::default()
},
};
element.render_loop().await?;
Ok(())
}
可以看到,整个 UI 结构就是一棵嵌套的 Element 树。每个区域(header、body、footer)都是一个 View,里面再嵌套 Text 组件。所有布局和样式都通过 props 直接声明,结构一目了然。
运行后效果如下:
练习:
请你尝试用同样的方式,基于新的 Element 体系,改写 Border 组件。要求:
你可以参考 View 组件的写法,思考如何把边框样式、子组件等信息通过 props 传递,并在 update 和 draw 方法中实现对应的逻辑。
通过这个练习,你将进一步掌握如何用统一的 Element 体系灵活组合和扩展自定义组件。
本节详细介绍了 Ratatui Kit 的组件更新机制,包括节点唯一标识、组件实例化简化、组件复用与最小化重建,以及统一的 Element 渲染主循环。通过 View 组件和新的 Element 体系,UI 结构声明和组件组合都变得更加清晰和灵活。这些设计让终端 UI 的开发体验大幅提升,也为后续实现更复杂的状态管理和交互能力打下了坚实基础。
下一步,我们将引入 Hook 系统,进一步完善组件的状态管理和响应机制,让你的终端 UI 也能拥有类似 React 的响应式开发体验,敬请期待。