【Xilem 0.4 基础语法学与练】第二课视图修饰符、列表渲染、组件拆分、memoize 性能优化

引言

截至2026年上半年,Xilem 仍处于 Alpha 阶段(0.4.0),API 可能随版本更新而变化。本课代码基于 0.4.x 编写,实际使用时请以最新文档为准。

一、视图修饰符(View Modifiers)

在第一课中,我们学会了用 text_button 的闭包参数处理点击事件,用 label 显示文本。但这些组件的"默认行为"往往不够------我们需要禁用按钮、给标签加点击事件、调整样式。Xilem 通过视图修饰符来解决这个问题。

1.1 什么是视图修饰符

视图修饰符是附加在 View 上的方法调用,它不改变原始 View 的类型,而是返回一个新的包装类型,在原始 View 的基础上叠加额外行为。

rust 复制代码
// 一个普通的按钮
text_button("点击我", |data: &mut AppState| { /* ... */ })

// 加了 .enabled() 修饰符的按钮------被禁用时不可点击
text_button("点击我", |data: &mut AppState| { /* ... */ })
    .enabled(data.is_active)

修饰符的核心思想是组合:

每个修饰符都是一个独立的"层",叠加在原始 View 之上,最终产出的仍然是一个实现了 WidgetView 的类型,可以继续嵌套、组合。

1.2 常用修饰符一览

.enabled(bool) ------ 控制可交互性

接受一个 bool 值。当值为 false 时,该 View 变为"禁用"状态------按钮不可点击、输入框不可编辑,视觉上通常变灰。

rust 复制代码
text_button("提交", |data: &mut AppState| data.submit())
    .enabled(!data.form.is_empty())

效果:只有当表单不为空时,"提交"按钮才可点击。enabled 的值每次 app_logic 重新执行时都会被重新求值,所以它是响应式的------状态变了,按钮的可用状态自动跟着变。

.on_click(closure) ------ 为任意 View 添加点击事件

text_button 自身已经接受一个点击回调,但 label、flex 容器等 View 默认不响应点击。.on_click() 可以让任何 View 变得可点击:

rust 复制代码
label("点我试试")
    .on_click(|data: &mut AppState| data.clicked = true)

也可以给一个 flex 容器整体加点击事件:

rust 复制代码
flex(Axis::Horizontal, (
    label("整行可点击"),
    label("→"),
))
.on_click(|data: &mut AppState| data.toggle_expand())

.tooltip(text) ------ 悬浮提示

鼠标悬停时显示提示文字:

rust 复制代码
text_button("?", |data: &mut AppState| data.show_help())
    .tooltip("点击显示帮助信息")

链式调用

修饰符可以链式叠加,顺序不影响语义:

rust 复制代码
label("操作")
    .on_click(|data: &mut AppState| data.do_action())
    .enabled(data.can_act)
    .tooltip("执行当前操作")

1.3 修饰符的工作原理

每个修饰符方法接收 self(原始 View),返回一个新的包装类型。例如 .enabled(true) 大致等价于:

rust 复制代码
Enabled<View> { inner: view, is_enabled: true }

这个 Enabled 同样实现了 WidgetView,所以可以继续调用其他修饰符,也可以放进 flex 的元组中作为子 View。这就是 Xilem 的组合式 API 设计------所有 View 都是可组合的"积木块"。

二、列表渲染(for_each / ViewSequence)

第一课中,我们把固定数量的子 View 放进元组:(label(...), text_button(...), text_button(...))。但实际应用中,列表的长度往往是动态的------待办事项有几条、购物车有几个商品,都取决于运行时数据。

2.1 for_each 基础用法

for_each 是 Xilem 提供的列表渲染函数。它接收两个参数:

  1. 一个迭代器(数据源)
  2. 一个闭包(将每条数据映射为一个 View)
rust 复制代码
struct AppState {
    items: Vec<String>,
}

fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        label(format!("共 {} 项", data.items.len())),
        for_each(data.items.iter(), |item, _index| {
            label(item.clone())
        }),
    ))
}

关键点:

  • data.items.iter() 产生一个 &String 的迭代器
  • 闭包的第一个参数 item 是迭代器产出的每一项(这里是 &String)
  • 闭包的第二个参数 _index 是当前项的索引(usize),不需要时可以加下划线忽略
  • 闭包返回的 View 类型必须一致------这里每项都是一个 label

2.2 带交互的列表项

列表项不只是展示文本,通常还需要响应点击、删除等操作:

rust 复制代码
struct TodoApp {
    todos: Vec<TodoItem>,
}

struct TodoItem {
    text: String,
    done: bool,
}

fn app_logic(data: &mut TodoApp) -> impl WidgetView<TodoApp> + use<> {
    flex(Axis::Vertical, (
        label("待办事项"),
        for_each(data.todos.iter().enumerate(), |(idx, todo), _key| {
            flex(Axis::Horizontal, (
                label(format!(
                    "{} {}",
                    if todo.done { "✅" } else { "⬜" },
                    todo.text
                )),
                text_button("删除", move |data: &mut TodoApp| {
                    data.todos.remove(idx);
                }),
            ))
        }),
    ))
}

关键点:

  • 用 .enumerate() 给迭代器加上索引,这样闭包中就能拿到 idx
  • 闭包参数变成 |(idx, todo), _key|------第一个参数是迭代器产出的 (usize, &TodoItem) 元组
  • 删除按钮的闭包用 move 关键字捕获 idx 的副本,这样即使列表后续变化,每个按钮记住的仍然是创建时的索引

2.3 ViewSequence trait

for_each 返回的类型实现了 ViewSequence trait。ViewSequence 是 Xilem 中处理"多个 View"的通用抽象------元组 (view1, view2, view3) 实现了它,for_each(...) 也实现了它。

这意味着 for_each 的返回值可以直接放进 flex 的元组中,和其他 View 混排:

rust 复制代码
flex(Axis::Vertical, (
    label("标题"),                           // 普通 View
    for_each(data.items.iter(), |item, _| {  // ViewSequence
        label(item.clone())
    }),
    text_button("添加", |data: &mut AppState| { /* ... */ }),  // 普通 View
))

2.4 列表渲染的性能特征

for_each 在底层通过 ViewSequence 的 diff 机制工作。当列表数据变化时,框架会比较新旧序列的差异,只更新实际变化的部分------新增的项会被创建,删除的项会被销毁,未变化的项会被复用。

对于大规模列表(成百上千项),Xilem 提供了虚拟滚动机制,只渲染可见区域内的项目,避免一次性创建所有 Widget。

三、组件拆分

当 app_logic 函数越来越长时,把所有 View 构建逻辑塞在一个函数里会变得难以维护。Xilem 的视图函数就是普通 Rust 函数,所以拆分方式和普通代码重构完全一样------提取子函数。

3.1 基本拆分方式

拆分前------所有逻辑堆在一个函数里:

rust 复制代码
fn app_logic(data: &mut TodoApp) -> impl WidgetView<TodoApp> + use<> {
    flex(Axis::Vertical, (
        // 标题栏
        flex(Axis::Horizontal, (
            label("待办事项"),
            text_button("全部清除", |data: &mut TodoApp| data.todos.clear()),
        )),
        // 列表
        for_each(data.todos.iter().enumerate(), |(idx, todo), _| {
            flex(Axis::Horizontal, (
                label(format!("{} {}", if todo.done { "✅" } else { "⬜" }, todo.text)),
                text_button("删除", move |data: &mut TodoApp| { data.todos.remove(idx); }),
            ))
        }),
        // 状态栏
        label(format!("共 {} 项,已完成 {} 项",
            data.todos.len(),
            data.todos.iter().filter(|t| t.done).count()
        )),
    ))
}

拆分后------每个区域独立为一个函数:

rust 复制代码
fn app_logic(data: &mut TodoApp) -> impl WidgetView<TodoApp> + use<> {
    flex(Axis::Vertical, (
        header_bar(data),
        todo_list(data),
        status_bar(data),
    ))
}

fn header_bar(data: &mut TodoApp) -> impl WidgetView<TodoApp> + use<> {
    flex(Axis::Horizontal, (
        label("待办事项"),
        text_button("全部清除", |data: &mut TodoApp| data.todos.clear()),
    ))
}

fn todo_list(data: &mut TodoApp) -> impl WidgetView<TodoApp> + use<> {
    for_each(data.todos.iter().enumerate(), |(idx, todo), _| {
        flex(Axis::Horizontal, (
            label(format!("{} {}", if todo.done { "✅" } else { "⬜" }, todo.text)),
            text_button("删除", move |data: &mut TodoApp| { data.todos.remove(idx); }),
        ))
    })
}

fn status_bar(data: &mut TodoApp) -> impl WidgetView<TodoApp> + use<> {
    label(format!("共 {} 项,已完成 {} 项",
        data.todos.len(),
        data.todos.iter().filter(|t| t.done).count()
    ))
}

3.2 拆分的规则

  • 子函数签名和 app_logic 完全一致:fn(data: &mut State) -> impl WidgetView + use<>
  • 参数仍然是 &mut State:子函数直接访问和修改同一个状态结构体,不需要额外传参
  • 子函数就是普通函数:可以在里面用 if/else、let、match,做任何 Rust 允许的操作
  • 子函数之间可以互相调用:就像普通函数一样

3.3 带参数的子组件

有时子组件不需要访问完整状态,只需要部分数据。这时可以传普通参数:

/// 渲染单个待办项------不需要 &mut TodoApp,只需要这一项的数据和索引

rust 复制代码
fn todo_item(text: &str, done: bool, idx: usize) -> impl WidgetView<TodoApp> + use<> {
    flex(Axis::Horizontal, (
        label(format!("{} {}", if done { "✅" } else { "⬜" }, text)),
        text_button("删除", move |data: &mut TodoApp| {
            data.todos.remove(idx);
        }),
    ))
}

fn todo_list(data: &mut TodoApp) -> impl WidgetView<TodoApp> + use<> {
    for_each(data.todos.iter().enumerate(), |(idx, todo), _| {
        todo_item(&todo.text, todo.done, idx)
    })
}

注意:

todo_item 的返回类型仍然是 impl WidgetView------它必须能操作完整的应用状态(因为"删除"按钮需要 &mut TodoApp),但它的输入参数只需要当前项的数据。

3.4 拆分的时机

不是所有代码都需要拆。建议遵循以下原则:

  • 超过 30 行的视图函数考虑拆分
  • 重复出现的 UI 模式提取为子组件
  • 逻辑上独立的区域(标题栏、列表、状态栏)各自独立

四、memoize 性能优化

4.1 问题场景

每次状态变化时,app_logic 会被重新执行,所有子 View 都会被重新构建。对于大多数 View(label、text_button),构建成本极低------只是创建一个轻量级的描述对象。但如果某个子 View 的构建涉及昂贵计算(比如格式化大量数据、生成复杂布局),每次都重新构建就会造成性能浪费。

rust 复制代码
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        // 这个列表有 1000 项,每次状态变化都重新构建所有项的 View
        for_each(data.items.iter(), |item, _| {
            expensive_view(item)
        }),
        // 其实只有这个计数器变了
        label(format!("计数: {}", data.counter)),
    ))
}

在上面的例子中,用户点击计数器按钮时,data.counter 变了,但 data.items 完全没变。理想情况下,列表部分应该被跳过,只更新计数标签。

4.2 memoize 的用法

memoize 是 Xilem 提供的性能优化工具。它的核心思想是:缓存子 View 的构建结果,只有当相关数据真正变化时才重新构建。

rust 复制代码
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        // 用 memoize 包裹列表部分
        memoize(
            data.items.clone(),  // 作为比较基准的数据
            |items: &Vec<Item>| {
                for_each(items.iter(), |item, _| {
                    expensive_view(item)
                })
            },
        ),
        // 计数器变化时,上面的 memoize 不会重新构建
        label(format!("计数: {}", data.counter)),
    ))
}

工作原理:

  1. memoize 接收两个参数:一个数据值和一个构建闭包
  2. 首次执行时,memoize 调用闭包构建 View,同时缓存传入的数据值
  3. 后续执行时,memoize 将新传入的数据与缓存的数据做 PartialEq 比较
  4. 如果数据没变→ 直接复用缓存的 View,跳过闭包调用
  5. 如果数据变了→ 重新调用闭包构建新 View,更新缓存

4.3 使用要求

  • 传给 memoize 的数据类型必须实现 PartialEq(用于比较新旧值是否相同)
  • 数据需要 Clone(memoize 内部需要持有数据的副本用于下次比较)
  • 闭包必须是纯函数------相同的输入必须产生相同的输出

4.4 什么时候该用 memoize

场景 是否需要 memoize

简单的 label、text_button ❌ 构建成本极低,不需要

几十个列表项 ❌ 通常不需要

成百上千项的列表 ✅ 列表数据未变时跳过重建

包含复杂计算结果的 View ✅ 避免重复计算

嵌套很深的子树 ✅ 减少 diff 开销

经验法则:先用着,遇到性能瓶颈时再用 memoize 优化。过早优化是万恶之源。

4.5 memoize 与组件拆分的关系

memoize 和组件拆分是正交的优化手段,可以同时使用:

rust 复制代码
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        // 拆分:独立的子函数
        // 优化:memoize 包裹,数据不变时跳过重建
        memoize(
            data.items.clone(),
            |items: &Vec<Item>| todo_list_section(items),
        ),
        counter_section(data),
    ))
}

五、综合实战:待办事项应用

把本课学到的四个知识点------修饰符、列表渲染、组件拆分、memoize------融合到一个完整的应用中。

5.1 完整代码

rust 复制代码
use xilem::view::{Axis, flex, for_each, label, text_button, memoize};
use xilem::{EventLoop, WidgetView, WindowOptions, Xilem};
use winit::error::EventLoopError;

// ===== 状态定义 =====

#[derive(Clone, PartialEq)]
struct TodoItem {
    text: String,
    done: bool,
}

struct AppState {
    todos: Vec<TodoItem>,
    input: String,
    next_id: usize,
}

impl Default for AppState {
    fn default() -> Self {
        AppState {
            todos: vec![
                TodoItem { text: "学习 Xilem".to_string(), done: false },
                TodoItem { text: "写一个待办应用".to_string(), done: false },
            ],
            input: String::new(),
            next_id: 2,
        }
    }
}

// ===== 主视图 =====

fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        header(),
        input_area(data),
        // memoize:只有 todos 变化时才重建列表
        memoize(
            data.todos.clone(),
            |todos: &Vec<TodoItem>| todo_list(todos),
        ),
        status_bar(data),
    ))
}

// ===== 子组件 =====

fn header() -> impl WidgetView<AppState> + use<> {
    label("📝 待办事项").enabled(true)
}

fn input_area(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Horizontal, (
        // 假设有一个 text_input 组件
        text_input(&data.input, |data: &mut AppState, text| {
            data.input = text;
        }),
        text_button("添加", |data: &mut AppState| {
            if !data.input.is_empty() {
                data.todos.push(TodoItem {
                    text: data.input.clone(),
                    done: false,
                });
                data.input.clear();
            }
        })
        .enabled(!data.input.is_empty()),  // 输入为空时禁用添加按钮
    ))
}

fn todo_list(todos: &[TodoItem]) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        for_each(todos.iter().enumerate(), |(idx, todo), _| {
            flex(Axis::Horizontal, (
                label(format!(
                    "{} {}",
                    if todo.done { "✅" } else { "⬜" },
                    todo.text
                ))
                // 点击文本切换完成状态
                .on_click(move |data: &mut AppState| {
                    data.todos[idx].done = !data.todos[idx].done;
                }),
                text_button("删除", move |data: &mut AppState| {
                    data.todos.remove(idx);
                }),
            ))
        }),
    ))
}

fn status_bar(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    let total = data.todos.len();
    let done = data.todos.iter().filter(|t| t.done).count();
    let tip = if total == 0 {
        "暂无待办事项"
    } else if done == total {
        "🎉 全部完成!"
    } else {
        ""
    };

    flex(Axis::Horizontal, (
        label(format!("共 {} 项,已完成 {} 项", total, done)),
        label(tip.to_string()),
        text_button("清除已完成", |data: &mut AppState| {
            data.todos.retain(|t| !t.done);
        })
        .enabled(done > 0),  // 没有已完成项时禁用
    ))
}

// ===== 启动 =====

fn main() -> Result<(), EventLoopError> {
    let app = Xilem::new_simple(
        AppState::default(),
        app_logic,
        WindowOptions::new("待办事项"),
    );
    app.run_in(EventLoop::with_user_event())?;
    Ok(())
}

5.2 代码结构说明

组件 职责 用到的知识点

app_logic 主视图,组合所有子组件 组件拆分

header 标题 修饰符 .enabled()

input_area 输入框 + 添加按钮 修饰符 .enabled()

todo_list 待办列表 for_each + .on_click()

status_bar 状态统计 + 清除按钮 条件渲染 + .enabled()

memoize 调用 包裹 todo_list memoize 性能优化

5.3 数据流回顾

用户点击"添加"按钮

→ 闭包执行:data.todos.push(...),data.input.clear()

→ 状态变化,app_logic 重新执行

→ memoize 检测到 data.todos 变了 → 重建 todo_list

→ input_area 重新执行 → 输入框清空,按钮状态更新

→ status_bar 重新执行 → 计数更新

六、练习

练习一:可禁用的加减计数器

在第一课的加减计数器基础上,添加以下功能:

  • 当数字 ≥ 10 时,"+1"按钮变为禁用状态(不可再点击)
  • 当数字 ≤ 0 时,"-1"按钮变为禁用状态
  • 添加一个"重置"按钮,将数字归零;当数字已经是 0 时,该按钮禁用

提示:使用 .enabled() 修饰符。

练习二:可删除的标签列表

定义状态:

struct TagApp {

tags: Vec,

}

初始时有 3 个标签:"Rust"、"Xilem"、"GUI"。

要求:

  • 用 for_each 渲染所有标签
  • 每个标签旁边有一个"×"删除按钮
  • 点击"×"后删除对应标签
  • 列表为空时,显示"暂无标签"

练习三:带 memoize 的成绩单

定义状态:

struct GradeBook {

students: Vec,

selected_course: String,

}

#derive(Clone, PartialEq)

struct Student {

name: String,

grades: Vec,

}

要求:

  • 将学生列表部分用 memoize 包裹(以 students 作为比较基准)
  • 切换 selected_course 时,学生列表不重建(因为 students 没变)
  • 添加/删除学生时,列表重建
  • 每个学生显示姓名和平均分

七、本课小结

知识点 核心概念 一句话记忆

视图修饰符 .enabled()、.on_click() 等方法叠加在 View 上 修饰符是 View 的"外壳",不改变内部结构

for_each 将迭代器的每一项映射为一个 View 动态列表用 for_each,固定列表用元组

组件拆分 将大视图函数拆成多个小函数 子函数签名和 app_logic 一致

memoize 缓存子 View,数据不变时跳过重建 先不优化,遇到瓶颈再用

下一课将介绍 Xilem 的 lens 机制------如何将大状态拆分为多个独立的子状态,让每个子组件只关注自己需要的那部分数据。

贴代码

练习一答案:可禁用的加减计数器

rust 复制代码
#[derive(Default)]
struct Counter {
    num: i32,
}

fn app_logic(data: &mut Counter) -> impl WidgetView<Counter> + use<> {
    flex(
        Axis::Vertical,
        (
            label(format!("{}", data.num)),
            flex(
                Axis::Horizontal,
                (
                    text_button("+1", |data: &mut Counter| data.num += 1)
                        .enabled(data.num < 10),
                    text_button("-1", |data: &mut Counter| data.num -= 1)
                        .enabled(data.num > 0),
                    text_button("重置", |data: &mut Counter| data.num = 0)
                        .enabled(data.num != 0),
                ),
            ),
        ),
    )
}

知识点:

  • .enabled() 接收一个 bool 表达式,每次 app_logic 重新执行时都会被重新求值------状态变了,按钮的可用状态自动跟着变,不需要手动"监听"。
  • 三个按钮各自独立判断启用条件:< 10、> 0、!= 0,互不干扰。
  • 多个修饰符可以链式调用。如果还需要加 .on_click() 或 .tooltip(),直接继续往后链即可,顺序不影响语义。

练习二答案:可删除的标签列表

rust 复制代码
struct TagApp {
    tags: Vec<String>,
}

impl Default for TagApp {
    fn default() -> Self {
        TagApp {
            tags: vec![
                "Rust".to_string(),
                "Xilem".to_string(),
                "GUI".to_string(),
            ],
        }
    }
}

fn app_logic(data: &mut TagApp) -> impl WidgetView<TagApp> + use<> {
    flex(
        Axis::Vertical,
        (
            label("标签管理"),
            one_of(
                !data.tags.is_empty(),
                // 有标签时:渲染列表
                for_each(data.tags.iter().enumerate(), |(idx, tag), _| {
                    flex(
                        Axis::Horizontal,
                        (
                            label(tag.clone()),
                            text_button("×", move |data: &mut TagApp| {
                                data.tags.remove(idx);
                            }),
                        ),
                    )
                }),
                // 无标签时:显示提示
                label("暂无标签"),
            ),
        ),
    )
}

知识点:

  • for_each 配合 .enumerate() 可以在闭包中拿到当前项的索引 idx,用于定位要删除的元素。
  • 删除按钮的闭包必须用 move 关键字------idx 是 usize(Copy 类型),move 会将 idx 的副本移入闭包,这样即使列表后续变化,每个按钮记住的仍然是创建时的索引。
  • 列表为空时需要显示"暂无标签",但 for_each(...) 和 label(...) 是不同的 Rust 类型,不能直接放在 if/else 的两个分支中。one_of 正是为此设计的------它接收一个 bool 条件和两个不同种类的 View,在运行时根据条件选择渲染哪一个。

练习三答案:带 memoize 的成绩单

rust 复制代码
use xilem::view::{Axis, flex, for_each, label, memoize};
use xilem::{EventLoop, WidgetView, WindowOptions, Xilem};
use winit::error::EventLoopError;

// ===== 状态定义 =====

#[derive(Clone, PartialEq)]
struct Student {
    name: String,
    grades: Vec<i32>,
}

struct GradeBook {
    students: Vec<Student>,
    selected_course: String,
}

impl Default for GradeBook {
    fn default() -> Self {
        GradeBook {
            students: vec![
                Student { name: "张三".to_string(), grades: vec![85, 92, 78] },
                Student { name: "李四".to_string(), grades: vec![90, 88, 95] },
                Student { name: "王五".to_string(), grades: vec![70, 65, 80] },
            ],
            selected_course: "数学".to_string(),
        }
    }
}

// ===== 主视图 =====

fn app_logic(data: &mut GradeBook) -> impl WidgetView<GradeBook> + use<> {
    flex(
        Axis::Vertical,
        (
            label(format!("当前课程: {}", data.selected_course)),
            flex(
                Axis::Horizontal,
                (
                    text_button("数学", |data: &mut GradeBook| {
                        data.selected_course = "数学".to_string();
                    }),
                    text_button("语文", |data: &mut GradeBook| {
                        data.selected_course = "语文".to_string();
                    }),
                    text_button("英语", |data: &mut GradeBook| {
                        data.selected_course = "英语".to_string();
                    }),
                ),
            ),
            // memoize:只有 students 变化时才重建学生列表
            memoize(
                data.students.clone(),
                |students: &Vec<Student>| student_list(students),
            ),
        ),
    )
}

// ===== 子组件 =====

fn student_list(students: &[Student]) -> impl WidgetView<GradeBook> + use<> {
    flex(
        Axis::Vertical,
        (
            for_each(students.iter(), |student, _| {
                let avg = if student.grades.is_empty() {
                    0
                } else {
                    student.grades.iter().sum::<i32>() / student.grades.len() as i32
                };
                flex(
                    Axis::Horizontal,
                    (
                        label(student.name.clone()),
                        label(format!("平均分: {}", avg)),
                    ),
                )
            }),
        ),
    )
}

// ===== 启动 =====

fn main() -> Result<(), EventLoopError> {
    let app = Xilem::new_simple(
        GradeBook::default(),
        app_logic,
        WindowOptions::new("成绩单"),
    );
    app.run_in(EventLoop::with_user_event())?;
    Ok(())
}

知识点:

  • memoize 的第一个参数是比较基准------data.students.clone()。每次 app_logic 重新执行时,memoize 会将新的 students 与缓存的旧值做 PartialEq 比较。
  • 切换课程时,只有 data.selected_course 变了,data.students 没变 → memoize 检测到数据相同 → 跳过 student_list 的重建,直接复用缓存的 View。
  • 添加/删除学生时,data.students 变了 → memoize 检测到数据不同 → 重新调用闭包构建新的学生列表。
  • 使用 memoize 的数据类型必须同时实现 Clone(memoize 内部需要持有副本用于下次比较)和 PartialEq(用于比较新旧值)。Student 结构体上的 #derive(Clone, PartialEq) 就是为此准备的。
  • memoize 和组件拆分是正交的优化手段------student_list 既是独立的子函数(组件拆分),又被 memoize 包裹(性能优化),两者互不冲突。

本课知识点总结

知识点 核心概念 一句话记忆

.enabled(bool) 控制 View 的可交互性,false 时禁用 每次渲染自动重新求值,状态驱动

.on_click(closure) 为任意 View 添加点击事件 label、flex 等默认不可点击的 View 都能用

for_each(iter, closure) 将迭代器的每一项映射为一个 View 动态列表用 for_each,固定列表用元组

.enumerate() + move 在列表项闭包中捕获索引 move 将 idx 的副本移入闭包,防止索引错位

one_of(cond, view_a, view_b) 条件渲染两个不同类型的 View 解决 if/else 两个分支返回不同类型的问题

组件拆分 将大视图函数拆成多个小函数 子函数签名和 app_logic 一致:fn(&mut S) -> impl WidgetView + use<>
memoize(data, closure) 缓存子 View,数据不变时跳过重建 先不优化,遇到瓶颈再用;数据需 Clone + PartialEq

相关推荐
szarron1 小时前
RF Demo Kit|NanoVNA 射频演示测试板完整上手教程,滤波器、衰减器、SOLT 校准学习板
开发语言·人工智能·学习·php·射频工程·频谱仪
A_nanda1 小时前
学习工业相机的大纲
学习·工业相机
边境悍匪1 小时前
蜗牛学苑 Java 智能体学习 Day39|SpringAI 会话存储、Function‑Call 工具调用、MCP 思维导图复盘
java·开发语言·spring boot·学习·阿里云
woodykissme2 小时前
齿轮测量系列——齿轮测量这二十年
学习·机械·齿轮检测
孙启超2 小时前
【AI开发之Rust】第 1 课:认识 Rust 与开发环境 —— 装好工具链,跑通第一个程序
开发语言·人工智能·后端·ai·rust·安全架构
sunshine22 girl2 小时前
Java学习一 环境配置3 Idea中的maven仓库,pom.xml文件配置
学习·maven·intellij-idea
GIS好难学4 小时前
想转GIS开发,企业会卡专业背景吗?
学习·gis·gis开发·webgis
老猿讲编程4 小时前
【Eclipse OpenSOVD学习之十四】差距清单与演进建议
学习·rust·eclipse·车载·sovd
传奇开心果编程4 小时前
【Xilem 0.4 基础语法学与练】第17课:Lens 机制——状态切片与子组件复用
学习·rust·前端框架