在上一节中,我们已经实现了一个基础的计数器 Demo,并分析了其渲染循环机制。接下来,我们将引入“组件化”思想,进一步提升代码的可维护性和扩展性。
随着 UI 结构的复杂化,所有渲染和状态逻辑都堆叠在主循环中会导致代码臃肿、难以维护。组件化的核心思想是:将 UI 拆分为独立、可复用的单元,每个组件只关注自身的渲染和状态。
这种模式不仅提升了代码的可读性和复用性,也为后续引入更高级的特性(如 Hook)打下基础。
我们可以将 UI 的不同部分封装为独立的渲染函数:
fn render_title() -> Line<'static> {
Line::from("Counter Application")
.style(Style::default().bold().light_blue())
.centered()
}
fn render_count(count: i32) -> Paragraph<'static> {
Paragraph::new(format!("Count: {}", count).light_green()).centered()
}
fn render_info() -> Line<'static> {
Line::from("Press q or Ctrl+C to quit, + to increase, - to decrease")
.style(Style::default().yellow())
.centered()
}
在主循环中,我们只需调用这些组件函数来渲染对应区域:
// ...existing code...
let title = render_title();
let text = render_count(count);
let info = render_info();
// ...existing code...
不过,这样的函数式组件用起来还是有不少槽点:
所以,我们需要一个能支持嵌套、自动分配区域、方便组合的组件化结构和布局系统,来解决这些问题。
要让组件能像树一样嵌套、组合,首先得有一套“组件协议”。我们可以新建一个 component
模块,先定义一个基础的组件 trait。因为每个组件可能需要不同的参数,所以用关联类型来描述 props。
component/mod.rs
里可以这样写:
pub trait Component: Any {
type Props<'a>
where
Self: 'a;
fn new(props: &Self::Props<'_>) -> Self;
fn draw(&self, _frame: &mut ratatui::Frame<'_>, _area: ratatui::layout::Rect) {}
}
由于 Component trait 包含了关联类型参数,因此它不具备对象安全性(Object Safety),无法直接作为 trait 对象使用。为此,我们需要定义一个新的对象安全 trait AnyComponent 来实现这一点。
pub trait AnyComponent {
fn draw(&self, frame: &mut ratatui::Frame<'_>, area: ratatui::layout::Rect);
}
// 为所有实现了 Component trait 的类型自动实现 AnyComponent trait
impl<T> AnyComponent for T
where
T: Component,
{
/// 调用具体组件的 draw 方法,实现多态分发
fn draw(&self, frame: &mut ratatui::Frame<'_>, area: ratatui::layout::Rect) {
Component::draw(self, frame, area);
}
}
然后,创建一个组件容器 InstantiatedComponent
来管理组件及其子组件:
use std::ops::{Deref, DerefMut};
use crate::component::AnyComponent;
pub struct InstantiatedComponent {
pub component: Box<dyn AnyComponent>,
pub children: Components,
}
#[derive(Default)]
pub struct Components {
pub components: Vec<InstantiatedComponent>,
}
impl Deref for Components {
type Target = Vec<InstantiatedComponent>;
fn deref(&self) -> &Self::Target {
&self.components
}
}
impl DerefMut for Components {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.components
}
}
|> 思考: 如何创建一个组件实例?
由于使用了 AnyComponent
trait,我们没法知道具体的组件类型,所以不能直接调用 new 方法。我们需要一种额外的方式保存组件的类型信息,同时需要一个抽象的 Props 类型来传递参数。
新建一个props
模块,定义一个类型擦除的容器AnyProps
,用于保存组件的props参数。
// 定义一个用于释放原始指针内存的 trait
trait DropRaw {
fn drop_raw(&self, raw: *mut ());
}
// 通过 PhantomData 标记类型 T 来保留结构体与泛型参数的关联关系
struct DropRawImpl<T> {
_marker: std::marker::PhantomData<T>,
}
// 为 DropRawImpl 实现 DropRaw trait
impl<T> DropRaw for DropRawImpl<T> {
// 将 *mut () 转换为 *mut T 并通过 Box::from_raw 构造智能指针来安全释放对应的堆内存
fn drop_raw(&self, raw: *mut ()) {
unsafe {
let _ = Box::from_raw(raw as *mut T);
}
}
}
// 一个可以持有任意类型属性的结构体
pub struct AnyProps<'a> {
raw: *mut (), // 指向实际数据的原始指针
drop: Option<Box<dyn DropRaw + 'a>>, // 用于释放 raw 所指向的数据
_marker: std::marker::PhantomData<&'a mut ()>, // 标记生命周期信息
}
impl<'a> AnyProps<'a> {
pub(crate) fn owned<T: 'a>(props: T) -> Self {
// 将堆分配的值转换为原始指针,用于手动内存管理
let raw = Box::into_raw(Box::new(props));
Self {
// 将 *mut T 转换为 *mut () 实现类型擦除
// 保留指向具体类型的指针信息,但隐藏具体类型
raw: raw as *mut (),
drop: Some(Box::new(DropRawImpl::<T> {
_marker: std::marker::PhantomData,
})),
_marker: std::marker::PhantomData,
}
}
pub(crate) fn borrowed<T>(props: &'a mut T) -> Self {
// 创建一个不负责内存释放的 AnyProps 实例
// 用于持有对 T 类型数据的引用
Self {
// 将 &mut T 转换为 *mut (),实现类型擦除
raw: props as *const _ as *mut (),
// 不负责内存释放,因此 drop 设置为 None
drop: None, // 不负责内存释放
// 使用 PhantomData 标记生命周期信息
_marker: std::marker::PhantomData,
}
}
// 创建一个新的 AnyProps 实例,共享当前实例的 raw 指针
// 不获取所有权,也不负责释放内存
pub(crate) fn borrow(&mut self) -> Self {
Self {
raw: self.raw,
drop: None, // 不负责内存释放
_marker: std::marker::PhantomData,
}
}
// 不安全地将内部指针向下转换为具体类型的不可变引用
// 必须保证当前 raw 指针确实指向 T 类型的数据
pub(crate) unsafe fn downcast_ref_unchecked<T>(&self) -> &T {
unsafe { &*(self.raw as *const T) }
}
// 不安全地将内部指针向下转换为具体类型的可变引用
// 必须保证当前 raw 指针确实指向 T 类型的数据
pub(crate) unsafe fn downcast_mut_unchecked<T>(&mut self) -> &mut T {
unsafe { &mut *(self.raw as *mut T) }
}
}
impl Drop for AnyProps<'_> {
fn drop(&mut self) {
// 如果 drop 字段存在,则调用其 drop_raw 方法释放内存
if let Some(drop) = self.drop.take() {
drop.drop_raw(self.raw);
}
}
}
小结:
在前面处理 props 类型擦除和内存释放时,我们采用了 DropRaw trait 搭配结构体的方式,解决了类型信息丢失后的资源管理问题。组件实例化其实也面临类似的挑战:类型擦除后,如何动态创建具体的组件实例?
这里我们同样借鉴 DropRaw 的思路,定义一个 trait(ComponentHelperExt
)和一个泛型结构体(ComponentHelper<T>
),专门用于保存组件类型信息和实例化方法。这样,每种组件类型都可以有一个对应的“工厂”,负责根据类型擦除的 props 创建具体组件。
在 component/component_helper.rs
中实现如下:
use crate::{
component::{AnyComponent, Component},
props::AnyProps,
};
// 组件实例化工厂 trait,支持类型擦除下的动态创建
pub trait ComponentHelperExt {
fn new_component(&self, props: AnyProps) -> Box<dyn AnyComponent>;
fn copy(&self) -> Box<dyn ComponentHelperExt>;
}
// 泛型结构体,保存组件类型信息
pub(crate) struct ComponentHelper<T> {
_marker: std::marker::PhantomData<T>,
}
impl<T> ComponentHelper<T>
where
T: Component,
{
pub fn boxed() -> Box<dyn ComponentHelperExt> {
Box::new(Self {
_marker: std::marker::PhantomData,
})
}
}
impl<T> ComponentHelperExt for ComponentHelper<T>
where
T: Component,
{
fn new_component(&self, props: AnyProps) -> Box<dyn AnyComponent> {
Box::new(T::new(unsafe { props.downcast_ref_unchecked() }))
}
fn copy(&self) -> Box<dyn ComponentHelperExt> {
Self::boxed()
}
}
通过这种设计,每个组件类型都能注册自己的实例化工厂,解决了类型擦除后无法直接 new 的问题。
接下来,扩展 InstantiatedComponent
,让它持有 helper,并提供统一的实例化方法:
pub struct InstantiatedComponent {
component: Box<dyn AnyComponent>,
children: Components,
helper: Box<dyn ComponentHelperExt>,
}
impl InstantiatedComponent {
pub fn new(mut props: AnyProps, helper: Box<dyn ComponentHelperExt>) -> Self {
let component = helper.new_component(props.borrow());
Self {
component,
children: Components::default(),
helper,
}
}
}
这样,InstantiatedComponent 只需持有 helper 和 props,就能动态创建和管理具体组件实例,并支持递归管理子组件。
本节我们梳理了组件化实现中的类型擦除、内存管理和动态实例化等关键技术,介绍了如何借助 trait+结构体的设计,让组件系统既灵活又安全。通过这些机制,组件可以像积木一样自由组合和复用,也为后续实现组件树的渲染与布局打下了坚实的基础。
下一节将聚焦于组件树的渲染与布局系统,介绍如何让组件能够嵌套、自动分配区域,并实现灵活的 UI 结构。