【Xilem 0.4 基础语法学与练】第15课:状态管理与 memoize 性能优化

一、为什么需要 memoize

在 Xilem 中,每当状态发生变化,框架会重新调用 app_logic 函数,生成一棵全新的 View Tree。框架再把新树和旧树做 diff,只更新变化的部分。

这个机制在简单应用中完全够用。但当应用变复杂时,问题就来了:

rust 复制代码
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        header(data),       // 标题栏
        todo_list(data),    // 待办列表(可能有 100 项)
        status_bar(data),   // 状态栏
    ))
}

假设用户只是在状态栏切换了"显示/隐藏已完成项"的过滤器,data.todos 本身没有变化。但 app_logic 重新执行时,todo_list(data) 也会被重新调用------它会重新生成 100 个列表项的 View 描述。虽然框架的 diff 会发现这些 View 没变从而跳过 Widget 更新,但生成 View Tree 本身的计算开销是白白浪费的。

memoize 就是用来解决这个问题的:如果某段 View 依赖的数据没有变化,就跳过 View 的重建,直接复用上一次的缓存。

二、memoize 的基本用法

2.1 最简单的例子

rust 复制代码
use xilem::view::{memoize, label, text_button, flex, Axis};
use xilem::WidgetView;

#[derive(Default)]
struct AppState {
    count: i32,
    label_text: String,
}

fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        // 这个 label 依赖 count,每次 count 变化都需要重建
        label(format!("计数: {}", data.count)),

        // 这个 label 依赖 label_text,和 count 无关
        // 用 memoize 包裹:只要 label_text 没变,就跳过重建
        memoize(data.label_text.clone(), |text: String| {
            label(format!("标题: {}", text))
        }),

        text_button("计数+1", |data: &mut AppState| {
            data.count += 1;
        }),
    ))
}

当用户点击"计数+1"按钮时:

  • data.count 变了 → label(format!("计数: {}", data.count)) 重新生成
  • data.label_text 没变 → memoize 检测到依赖数据未变 → 跳过内部闭包的执行,直接复用上一次的 View

2.2 memoize 的签名

rust 复制代码
fn memoize<State, View, F>(data: State, view_fn: F) -> Memoize<...>
where
    F: Fn(State) -> View,

memoize 接受两个参数:

  • data:依赖的数据,框架用它来判断"是否需要重建"
  • view_fn:一个闭包,接收 data,返回 View。只有当 data 变化时才会被调用

三、状态管理的核心机制

##3.1 数据流回顾

Xilem 的数据流是一个单向循环:

用户操作(点击按钮)

→ 闭包执行,修改了 AppState 中的某些字段

→ 框架检测到状态变化,重新调用 app_logic

→ app_logic 生成新的 View Tree

→ 框架 diff 新旧 View Tree

→ 只更新发生变化的 Widget

→ 屏幕刷新

关键理解:闭包中对 &mut AppState 的修改,会触发框架重新调用 app_logic。 你不需要手动通知框架"状态变了",修改就是通知。

3.2 状态的粒度决定重建的范围

rust 复制代码
#[derive(Default)]
struct AppState {
    todos: Vec<TodoItem>,      // 列表数据
    filter: FilterMode,        // 过滤器
    input_text: String,        // 输入框文本
}

当用户修改 input_text 时,整个 app_logic 都会重新执行。如果 todo_list 没有用 memoize 包裹,它也会被重新生成------即使 todos 完全没变。

这就是为什么需要 memoize:它让你精确控制"哪些数据变化才触发哪些 View 的重建"。

四、实战:带 memoize 的待办事项应用

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

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

#[derive(Default)]
struct AppState {
    todos: Vec<TodoItem>,
    next_id: u32,
    input_text: String,
    show_completed: bool,
}

// ===== 子组件:标题 =====
fn header() -> impl WidgetView<AppState> + use<> {
    label("📋 待办事项".to_string())
}

// ===== 子组件:输入区域 =====
fn input_area(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Horizontal, (
        label(format!("[{}]", data.input_text)),
        text_button("添加", |data: &mut AppState| {
            if !data.input_text.is_empty() {
                data.todos.push(TodoItem {
                    id: data.next_id,
                    text: data.input_text.clone(),
                    done: false,
                });
                data.next_id += 1;
                data.input_text.clear();
            }
        })
        .enabled(!data.input_text.is_empty()),
    ))
}

// ===== 子组件:待办列表 =====
// 这个组件依赖 data.todos,比较"重"
fn todo_list(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    let filtered: Vec<TodoItem> = data.todos.iter()
        .filter(|t| data.show_completed || !t.done)
        .cloned()
        .collect();

    for_each(filtered, |todo: &TodoItem| todo.id, |todo: TodoItem, _idx: usize| {
        flex(Axis::Horizontal, (
            label(format!(
                "{} {}",
                if todo.done { "✅" } else { "⬜" },
                todo.text
            )),
            text_button("删除", move |data: &mut AppState| {
                data.todos.retain(|t| t.id != todo.id);
            }),
        ))
    })
}

// ===== 子组件:状态栏 =====
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();

    flex(Axis::Horizontal, (
        label(format!("共 {} 项,已完成 {} 项", total, done)),
        text_button(
            if data.show_completed { "隐藏已完成" } else { "显示已完成" },
            |data: &mut AppState| {
                data.show_completed = !data.show_completed;
            },
        ),
    ))
}

// ===== 主视图 =====
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        header(),
        input_area(data),

        // ★ 关键:用 memoize 包裹 todo_list
        // 依赖数据是 data.todos 的克隆
        // 只有当 todos 真正变化时,todo_list 才会重新生成
        memoize(data.todos.clone(), move |todos: Vec<TodoItem>| {
            // 注意:memoize 闭包内无法直接访问 data
            // 如果 todo_list 还需要其他状态(如 show_completed),
            // 需要把它们也包含在依赖数据中
            for_each(todos, |todo: &TodoItem| todo.id, |todo: TodoItem, _idx: usize| {
                flex(Axis::Horizontal, (
                    label(format!(
                        "{} {}",
                        if todo.done { "✅" } else { "⬜" },
                        todo.text
                    )),
                    text_button("删除", move |data: &mut AppState| {
                        data.todos.retain(|t| t.id != todo.id);
                    }),
                ))
            })
        }),

        status_bar(data),
    ))
}

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

数据流分析

场景一:用户点击"添加"按钮

  • 闭包执行:data.todos.push(...) 修改了 data.todos,data.input_text.clear() 修改了输入文本
  • 状态变化,框架重新调用 app_logic
  • memoize 检测到 data.todos 变了 → 重建列表
  • input_area 重新执行 → 输入框清空,按钮状态更新
  • status_bar 重新执行 → 计数更新

场景二:用户点击"显示已完成/隐藏已完成"

  • 闭包执行:data.show_completed 被切换
  • 状态变化,框架重新调用 app_logic
  • memoize 检测到 data.todos 没变 → 跳过列表重建,直接复用缓存
  • status_bar 重新执行 → 按钮文字更新

这就是 memoize 的价值:切换过滤器时,列表可能有几十上百项,但 memoize 让它们免于重建。

五、memoize 的使用原则

5.1 什么时候用 memoize

适合用的场景:

  • 子组件依赖的数据量大(如长列表)
  • 子组件的 View 构建开销高(如复杂的嵌套布局)
  • 父状态中有多个独立的子状态,修改其中一个不应该触发其他子组件的重建

不需要用的场景:

  • 简单的 label、button 等轻量级组件------重建开销可以忽略
  • 子组件依赖的数据几乎每次都会变------缓存没有意义

5.2 依赖数据的选择

memoize 的第一个参数决定了"什么变化才触发重建"。选择依赖数据时要遵循一个原则:只包含闭包内实际用到的数据。

// ✅ 正确:只依赖 todos

memoize(data.todos.clone(), |todos: Vec| {

// 只用 todos 构建列表

...

})

// ❌ 错误:依赖了整个 data,任何字段变化都会触发重建

memoize(data.clone(), |data: AppState| {

// 虽然只用了 data.todos,但依赖了整个 data

...

})

5.3 memoize 闭包内的限制

memoize 的闭包只能访问通过第一个参数传入的数据,不能直接访问外部的 &mut AppState。这是因为 memoize 的核心思想是"数据不变就跳过"------如果闭包能访问外部可变状态,那"数据不变"就失去了意义。

如果你的子组件需要访问多个状态字段,把它们打包成一个结构体传入:

rust 复制代码
// 把多个依赖数据打包
#[derive(Clone)]
struct ListDeps {
    todos: Vec<TodoItem>,
    show_completed: bool,
}

// 传入打包后的数据
memoize(
    ListDeps {
        todos: data.todos.clone(),
        show_completed: data.show_completed,
    },
    |deps: ListDeps| {
        let filtered: Vec<_> = deps.todos.iter()
            .filter(|t| deps.show_completed || !t.done)
            .collect();
        // ... 构建列表
    },
)

这样,只有当 todos 或 show_completed 变化时,列表才会重建。

六、常见误区

误区一:到处都用 memoize

memoize 本身也有开销------它需要存储缓存、比较数据。对于简单的 label("hello"),用 memoize 包裹反而更慢。

原则:先不用,遇到性能瓶颈再加。

误区二:依赖数据选太宽

// ❌ 依赖了整个 AppState,任何字段变化都触发重建

memoize(data.clone(), |data: AppState| { ... })

这等于没有用 memoize,因为 app_logic 每次被调用时 data 都可能不同。

误区三:以为 memoize 能替代 diff

memoize 跳过的是 View Tree 的构建(Rust 代码的执行),而框架的 diff 跳过的是 Widget 的更新(底层渲染操作)。两者是不同层级的优化,互相补充:

  • memoize:避免重新执行 Rust 代码生成 View
  • diff:避免重新操作底层 Widget

即使不用 memoize,diff 也能保证"View 没变就不更新 Widget"。但 View 的构建本身是有开销的,memoize 省的是这部分开销。

七、练习

练习一:带 memoize 的计数器

rust 复制代码
#[derive(Default)]
struct AppState {
    count: i32,
    title: String,
}

要求:

  • title 用 memoize 包裹,只有 title 变化时才重建
  • 点击"计数+1"按钮时,title 不重建
  • 提供一个输入框修改 title(简化为按钮切换标题)

补全代码:

rust 复制代码
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        // 你的代码:用 memoize 包裹标题
        // ???

        label(format!("计数: {}", data.count)),

        text_button("计数+1", |data: &mut AppState| {
            data.count += 1;
        }),

        text_button("切换标题", |data: &mut AppState| {
            data.title = if data.title == "默认标题" {
                "新标题".to_string()
            } else {
                "默认标题".to_string()
            };
        }),
    ))
}

点击查看答案

rust 复制代码
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        memoize(data.title.clone(), |title: String| {
            label(format!("标题: {}", title))
        }),

        label(format!("计数: {}", data.count)),

        text_button("计数+1", |data: &mut AppState| {
            data.count += 1;
        }),

        text_button("切换标题", |data: &mut AppState| {
            data.title = if data.title == "默认标题" {
                "新标题".to_string()
            } else {
                "默认标题".to_string()
            };
        }),
    ))
}

练习二:多区域独立更新

rust 复制代码
#[derive(Default)]
struct AppState {
    todos: Vec<String>,
    theme: String,       // "light" 或 "dark"
    user_name: String,
}

要求:

  • 顶部显示用户名(依赖 user_name)
  • 中间显示待办列表(依赖 todos),用 memoize 包裹
  • 底部显示主题切换按钮(依赖 theme)
  • 切换主题时,待办列表不重建
  • 添加待办时,用户名和主题区域不重建(它们本身很轻量,不需要 memoize,但理解数据流很重要)

补全代码:

rust 复制代码
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        // 你的代码
    ))
}

点击查看答案

rust 复制代码
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        // 用户名区域(轻量,不需要 memoize)
        label(format!("用户: {}", data.user_name)),

        // 待办列表(用 memoize 包裹)
        memoize(data.todos.clone(), |todos: Vec<String>| {
            for_each(todos, |todo: &String| todo.clone(), |todo: String, _idx: usize| {
                label(todo)
            })
        }),

        // 主题切换
        flex(Axis::Horizontal, (
            label(format!("主题: {}", data.theme)),
            text_button("切换主题", |data: &mut AppState| {
                data.theme = if data.theme == "light" {
                    "dark".to_string()
                } else {
                    "light".to_string()
                };
            }),
        )),

        // 添加待办
        text_button("添加待办", |data: &mut AppState| {
            data.todos.push(format!("待办 {}", data.todos.len() + 1));
        }),
    ))
}

练习三:思考题------memoize 的边界

以下代码中,memoize 能否正常工作?为什么?

rust 复制代码
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        memoize(data.todos.clone(), |todos: Vec<TodoItem>| {
            // 闭包内尝试访问 data.show_completed
            // 但 data 不在这个闭包的作用域内
            let filtered: Vec<_> = todos.iter()
                .filter(|t| data.show_completed || !t.done)  // ← 这里会怎样?
                .collect();
            // ...
        }),
    ))
}

点击查看答案

这段代码无法编译。

memoize 的闭包只能访问通过第一个参数传入的数据(这里是 todos: Vec),不能访问外部的 data: &mut AppState。这是 Rust 的所有权规则决定的------data 的可变借用已经被 app_logic 持有,闭包不能再借用它。

正确的做法是把 show_completed 也打包进依赖数据:

rust 复制代码
#[derive(Clone)]
struct ListDeps {
    todos: Vec<TodoItem>,
    show_completed: bool,
}

memoize(
    ListDeps {
        todos: data.todos.clone(),
        show_completed: data.show_completed,
    },
    |deps: ListDeps| {
        let filtered: Vec<_> = deps.todos.iter()
            .filter(|t| deps.show_completed || !t.done)
            .collect();
        // ...
    },
)

八、本课小结

状态管理的核心是理解 Xilem 的单向数据流:闭包修改 &mut AppState → 框架重新调用 app_logic → 生成新 View Tree → diff → 更新 Widget。你不需要手动通知框架,修改状态本身就是通知。

memoize 是 View 层的性能优化工具,它缓存子 View 的构建结果,当依赖数据未变时跳过重建。它不是必须的------先写出正确的代码,遇到性能瓶颈时再加。

使用 memoize 的三个要点:依赖数据要尽量精确(只包含闭包内实际用到的);闭包内不能访问外部可变状态,需要的所有数据都通过第一个参数传入;轻量级组件不需要 memoize,过度使用反而增加开销。

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

练习答案和详细解读

练习一:带 memoize 的计数器

rust 复制代码
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        memoize(data.title.clone(), |title: String| {
            label(format!("标题: {}", title))
        }),

        label(format!("计数: {}", data.count)),

        text_button("计数+1", |data: &mut AppState| {
            data.count += 1;
        }),

        text_button("切换标题", |data: &mut AppState| {
            data.title = if data.title == "默认标题" {
                "新标题".to_string()
            } else {
                "默认标题".to_string()
            };
        }),
    ))
}

解读:memoize 的第一个参数是 data.title.clone(),这意味着只有当 title 的值发生变化时,内部的 label 才会重新构建。当用户点击"计数+1"按钮时,data.count 变了但 data.title 没变,memoize 检测到依赖数据未变,直接跳过闭包执行,复用上一次的 label View。这就是 memoize 的核心价值------精确控制重建范围。

练习二:多区域独立更新

rust 复制代码
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        // 用户名区域(轻量,不需要 memoize)
        label(format!("用户: {}", data.user_name)),

        // 待办列表(用 memoize 包裹)
        memoize(data.todos.clone(), |todos: Vec<String>| {
            for_each(todos, |todo: &String| todo.clone(), |todo: String, _idx: usize| {
                label(todo)
            })
        }),

        // 主题切换
        flex(Axis::Horizontal, (
            label(format!("主题: {}", data.theme)),
            text_button("切换主题", |data: &mut AppState| {
                data.theme = if data.theme == "light" {
                    "dark".to_string()
                } else {
                    "light".to_string()
                };
            }),
        )),

        // 添加待办
        text_button("添加待办", |data: &mut AppState| {
            data.todos.push(format!("待办 {}", data.todos.len() + 1));
        }),
    ))
}

解读:这个练习的关键是理解"哪些组件需要 memoize,哪些不需要"。

用户名区域只是一个简单的 label,重建开销几乎为零,不需要 memoize。主题切换区域同理。

待办列表用 for_each 渲染,当列表项多时,重建开销较大。用 memoize(data.todos.clone(), ...) 包裹后,只有当 todos 真正变化时(添加、删除、修改待办项),列表才会重建。当用户点击"切换主题"按钮时,只有 theme 变了,todos 没变,列表免于重建。

注意:用户名和主题区域虽然没有用 memoize,但这不影响正确性------它们确实会在每次状态变化时重新执行,但因为它们只是一个 label,开销可以忽略。如果用户名区域也很复杂(比如包含头像、个人简介等),那就也应该加 memoize(data.user_name.clone(), ...)。

练习三:memoize 的边界

rust 复制代码
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
    flex(Axis::Vertical, (
        memoize(data.todos.clone(), |todos: Vec<TodoItem>| {
            let filtered: Vec<_> = todos.iter()
                .filter(|t| data.show_completed || !t.done)  // ← 编译错误
                .collect();
            // ...
        }),
    ))
}

答案:这段代码无法编译。

解读:memoize 的闭包只能访问通过第一个参数传入的数据(这里是 todos: Vec),不能访问外部的 data: &mut AppState。原因有两层:

第一层是 Rust 的所有权规则。app_logic 的参数 data: &mut AppState 是一个可变借用,这个借用已经被 app_logic 函数持有。memoize 的闭包如果想再借用 data,就会形成"一个可变借用被两处同时持有"的局面,编译器不允许。

第二层是 memoize 的设计意图。memoize 的核心思想是"依赖数据不变就跳过重建"。如果闭包能访问外部的可变状态,那"数据不变"这个前提就失去了意义------闭包可能每次都读到不同的外部状态,导致缓存失效。

正确做法是把 show_completed 也打包进依赖数据:

rust 复制代码
#[derive(Clone)]
struct ListDeps {
    todos: Vec<TodoItem>,
    show_completed: bool,
}

memoize(
    ListDeps {
        todos: data.todos.clone(),
        show_completed: data.show_completed,
    },
    |deps: ListDeps| {
        let filtered: Vec<_> = deps.todos.iter()
            .filter(|t| deps.show_completed || !t.done)
            .collect();
        // ...
    },
)

这样,只有当 todos 或 show_completed 中任意一个发生变化时,列表才会重建。两个字段都被明确地纳入了依赖数据的范围,memoize 的缓存判断是完整且正确的。

知识要点

要点一:Xilem 的单向数据流

Xilem 的状态管理遵循严格的单向数据流:用户操作触发闭包 → 闭包修改 &mut AppState → 框架检测到状态变化 → 重新调用 app_logic → 生成新的 View Tree → diff 新旧 View Tree → 只更新变化的 Widget → 屏幕刷新。

你不需要手动通知框架"状态变了",对 &mut AppState 的修改本身就是通知。这和 React 的 setState、Vue 的响应式系统不同------Xilem 没有额外的通知机制,修改即通知。

要点二:memoize 解决的是 View Tree 构建开销

memoize 跳过的是 View Tree 的构建(Rust 代码的执行),而不是 Widget 的更新。框架的 diff 机制已经能保证"View 没变就不更新 Widget",但 View 的构建本身是有开销的------for_each 遍历列表、format! 格式化字符串、闭包调用等,这些都是 CPU 时间。memoize 省的就是这部分开销。

两者的关系是互补的:memoize 在上层避免重新执行 Rust 代码,diff 在下层避免重新操作底层 Widget。即使不用 memoize,diff 也能保证正确性,只是效率低一些。

要点三:依赖数据的选择原则

memoize 的第一个参数决定了"什么变化才触发重建"。选择依赖数据时遵循一个原则:只包含闭包内实际用到的数据。

依赖数据选太窄------闭包内用到了某个字段但没放进依赖数据,会导致数据变了但 View 不更新,产生 bug。

依赖数据选太宽------把整个 AppState 都放进去,那任何字段变化都会触发重建,等于没有用 memoize。

如果闭包需要多个字段,把它们打包成一个结构体传入,而不是依赖整个 AppState。

要点四:memoize 闭包的作用域限制

memoize 的闭包只能访问通过第一个参数传入的数据,不能访问外部的 &mut AppState。这是 Rust 所有权规则和 memoize 设计意图共同决定的。需要的所有数据都必须通过第一个参数显式传入。

要点五:什么时候用 memoize,什么时候不用

适合用的场景:子组件依赖的数据量大(如长列表);子组件的 View 构建开销高(如复杂的嵌套布局);父状态中有多个独立的子状态,修改其中一个不应该触发其他子组件的重建。

不需要用的场景:简单的 label、button 等轻量级组件,重建开销可以忽略;子组件依赖的数据几乎每次都会变,缓存没有意义。

总原则:先不用,遇到性能瓶颈再加。 memoize 本身也有开销------存储缓存、比较数据、克隆依赖数据。对于简单的 View,这些开销可能比直接重建还大。

相关推荐
具身AGI2 小时前
视频即仿真,物理AI 人类学习路线 的下一步
人工智能·学习
wuyk5553 小时前
从零吃透 MQTT 通信|第 8 章 FreeRTOS 多任务架构下 MQTT 工程架构,任务拆分、队列解耦、临界区保护
c语言·开发语言·stm32·学习·架构
陈年老古董3 小时前
PyTorch食物图像分类实战:从数据集制作到CNN模型训练全流程详解
pytorch·深度学习·学习·机器学习·分类·cnn
明志数科3 小时前
从300克Ego头环看第一人称数据采集趋势:设备轻量化之后,场景端壁垒在哪
数码相机·学习
笨鸟先飞的橘猫4 小时前
系统设计第十七天决策卡
学习·游戏
Sunshing154 小时前
Lyapunov方程系统本身稳定性判定与镇定性判定
笔记·学习
sunoo-2296 小时前
51单片机学习Day2:数码管动态扫描、定时器与中断系统深度总
单片机·嵌入式硬件·学习·51单片机
宣宣猪的小花园.6 小时前
【机器学习】从机理建模到数据学习:机器学习究竟替代了什么
人工智能·学习·机器学习
泯泷7 小时前
手搓JSVM第 12 篇:完整最小 JSVM 实现与源码设计复盘
前端·javascript·前端框架