一、为什么需要 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,这些开销可能比直接重建还大。