迭代
Iteration
无论是列出待办事项、显示表格还是展示产品图片,在 Web 应用程序中,迭代项目列表都是一项常见的任务。协调不断变化的项目集之间的差异也是框架处理得好坏最棘手的任务之一。
Whether you’re listing todos, displaying a table, or showing product images, iterating over a list of items is a common task in web applications. Reconciling the differences between changing sets of items can also be one of the trickiest tasks for a framework to handle well.
Leptos 支持两种不同的项目迭代模式:
Leptos supports two different patterns for iterating over items:
-
对于静态视图:
Vec<_> -
For static views:
Vec<_> -
对于动态列表:
<For/> -
For dynamic lists:
<For/>
使用 Vec<_> 处理静态视图
Static Views with Vec<_>
有时你需要重复显示一个项目,但你提取的列表并不经常变化。在这种情况下,重要的是要知道你可以将任何 Vec<IV> where IV: IntoView 插入到你的视图中。换句话说,如果你能渲染 T,你就能渲染 Vec<T>。
Sometimes you need to show an item repeatedly, but the list you’re drawing from does not often change. In this case, it’s important to know that you can insert any Vec<IV> where IV: IntoView into your view. In other words, if you can render T, you can render Vec<T>.
let values = vec![0, 1, 2];
view! {
// 这将直接渲染成 "012"
// this will just render "012"
<p>{values.clone()}</p>
// 或者我们可以将它们包裹在 <li> 中
// or we can wrap them in <li>
<ul>
{values.into_iter()
.map(|n| view! { <li>{n}</li>})
.collect::<Vec<_>>()}
</ul>
}
Leptos 还提供了一个 .collect_view() 辅助函数,允许你将任何 T: IntoView 的迭代器收集到 Vec<View> 中。
Leptos also provides a .collect_view() helper function that allows you to collect any iterator of T: IntoView into Vec<View>.
let values = vec![0, 1, 2];
view! {
// 这将直接渲染成 "012"
// this will just render "012"
<p>{values.clone()}</p>
// 或者我们可以将它们包裹在 <li> 中
// or we can wrap them in <li>
<ul>
{values.into_iter()
.map(|n| view! { <li>{n}</li>})
.collect_view()}
</ul>
}
列表是静态的,并不意味着界面也必须是静态的。你可以在静态列表中渲染动态项目。
The fact that the list is static doesn’t mean the interface needs to be static. You can render dynamic items as part of a static list.
// 创建一个包含 5 个信号的列表
// create a list of 5 signals
let length = 5;
let counters = (1..=length).map(|idx| RwSignal::new(idx));
请注意,这里我们没有调用 signal() 来获取包含 reader 和 writer 的元组,而是使用 RwSignal::new() 来获取一个读写信号。这在我们需要到处传递元组的情况下会更方便。
Note here that instead of calling signal() to get a tuple with a reader and a writer, here we use RwSignal::new() to get a single, read-write signal. This is just more convenient for a situation where we’d otherwise be passing the tuples around.
// 每个项目管理一个响应式视图
// each item manages a reactive view
// 但列表本身永远不会改变
// but the list itself will never change
let counter_buttons = counters
.map(|count| {
view! {
<li>
<button
on:click=move |_| *count.write() += 1
>
{count}
</button>
</li>
}
})
.collect_view();
view! {
<ul>{counter_buttons}</ul>
}
你也可以响应式地渲染 Fn() -> Vec<_>。但请注意,这是一个无键(unkeyed)列表更新:它将重用现有的 DOM 元素,并根据它们在新的 Vec<_> 中的顺序用新值更新它们。如果你只是在列表末尾添加和删除项目,这种方式效果很好,但如果你在移动项目或在列表中间插入项目,这将导致浏览器做多余的工作,并可能对输入状态和 CSS 动画等方面产生令人惊讶的影响。(关于“有键(keyed)”与“无键(unkeyed)”区别的更多信息及一些实际示例,你可以阅读这篇文章。)
You can render a Fn() -> Vec<_> reactively as well. But note that this is an unkeyed list update: it will reuse the existing DOM elements, and update them with the new values, according to their order in the new Vec<_>. If you’re just adding and removing items at the end of the list, this works well, but if you are moving items around or inserting items into the middle of the list, this will cause the browser to do more work than it needs to, and may have surprising effects on things like input state and CSS animations. (For more on the “keyed” vs. “unkeyed” distinction, and some practical examples, you can read this article.)
幸运的是,还有一种高效的方法可以进行有键列表迭代。
Luckily, there’s an efficient way to do keyed list iteration, as well.
使用 <For/> 组件进行动态渲染
Dynamic Rendering with the <For/> Component
<For/> 组件是一个有键动态列表。它接受三个 props:
The <For/> component is a keyed dynamic list. It takes three props:
-
each:一个返回要迭代的项目T的响应式函数 -
each: a reactive function that returns the itemsTto be iterated over -
key:一个接收&T并返回稳定且唯一的键或 ID 的键函数 -
key: a key function that takes&Tand returns a stable, unique key or ID -
children:将每个T渲染为视图 -
children: renders eachTinto a view
key 就是那个关键的键。你可以在列表中添加、删除和移动项目。只要每个项目的键随着时间的推移保持稳定,框架就不需要重新渲染任何项目(除非它们是新添加的),并且它可以非常高效地添加、删除和移动发生变化的项目。这允许在列表发生变化时进行极其高效的更新,且只需极少的额外工作。
key is, well, the key. You can add, remove, and move items within the list. As long as each item’s key is stable over time, the framework does not need to rerender any of the items, unless they are new additions, and it can very efficiently add, remove, and move items as they change. This allows for extremely efficient updates to the list as it changes, with minimal additional work.
创建一个好的 key 可能会有点棘手。通常你不希望为此目的使用索引,因为它不稳定——如果你删除或移动项目,它们的索引就会改变。
Creating a good key can be a little tricky. You generally do not want to use an index for this purpose, as it is not stable—if you remove or move items, their indices change.
但一个很好的主意是在生成每一行时为其生成一个唯一的 ID,并将其作为键函数的 ID。
But it’s a great idea to do something like generating a unique ID for each row as it is generated, and using that as an ID for the key function.
查看下面的 <DynamicList/> 组件示例。
Check out the <DynamicList/> component below for an example.
CodeSandbox Source
use leptos::prelude::*;
// 迭代在大多数应用中都是一项非常常见的任务。
// Iteration is a very common task in most applications.
// 那么,如何获取数据列表并将其渲染到 DOM 中呢?
// So how do you take a list of data and render it in the DOM?
// 这个例子将向你展示两种方式:
// This example will show you the two ways:
// 1) 对于大部分是静态的列表,使用 Rust 迭代器
// 1) for mostly-static lists, using Rust iterators
// 2) 对于会增长、收缩或移动项目的列表,使用 <For/>
// 2) for lists that grow, shrink, or move items, using <For/>
#[component]
fn App() -> impl IntoView {
view! {
<h1>"Iteration"</h1>
<h2>"Static List"</h2>
<p>"如果列表本身是静态的,请使用这种模式。"</p>
<StaticList length=5/>
<h2>"Dynamic List"</h2>
<p>"如果列表中的行会发生变化,请使用这种模式。"</p>
<DynamicList initial_length=5/>
}
}
/// 一个计数器列表,没有
/// 添加或删除功能。
/// A list of counters, without the ability
/// to add or remove any.
#[component]
fn StaticList(
/// 列表中包含的计数器数量。
/// How many counters to include in this list.
length: usize,
) -> impl IntoView {
// 创建以递增数字开始的计数器信号
// create counter signals that start at incrementing numbers
let counters = (1..=length).map(|idx| RwSignal::new(idx));
// 当你有一个不改变的列表时,你可以
// 使用普通的 Rust 迭代器来操作它
// when you have a list that doesn't change, you can
// manipulate it using ordinary Rust iterators
// 并将其收集到一个 Vec<_> 中以插入 DOM
// and collect it into a Vec<_> to insert it into the DOM
let counter_buttons = counters
.map(|count| {
view! {
<li>
<button
on:click=move |_| *count.write() += 1
>
{count}
</button>
</li>
}
})
.collect::<Vec<_>>();
// 注意,如果 `counter_buttons` 是一个响应式列表
// 并且其值发生了变化,这将会非常低效:
// 它在每次列表改变时都会重新渲染每一行。
// Note that if `counter_buttons` were a reactive list
// and its value changed, this would be very inefficient:
// it would rerender every row every time the list changed.
view! {
<ul>{counter_buttons}</ul>
}
}
/// 一个允许添加或
/// 删除计数器的计数器列表。
/// A list of counters that allows you to add or
/// remove counters.
#[component]
fn DynamicList(
/// 初始计数器数量。
/// The number of counters to begin with.
initial_length: usize,
) -> impl IntoView {
// 这个动态列表将使用 <For/> 组件。
// This dynamic list will use the <For/> component.
// <For/> 是一个有键列表。这意味着每一行
// 都有一个定义的键。如果键不改变,该行
// 就不会被重新渲染。当列表改变时,只有
// 最小数量的更改会被应用到 DOM。
// <For/> is a keyed list. This means that each row
// has a defined key. If the key does not change, the row
// will not be re-rendered. When the list changes, only
// the minimum number of changes will be made to the DOM.
// `next_counter_id` 将让我们生成唯一的 ID
// 我们通过在每次创建计数器时简单地将 ID 加一来做到这一点
// `next_counter_id` will let us generate unique IDs
// we do this by simply incrementing the ID by one
// each time we create a counter
let mut next_counter_id = initial_length;
// 我们像在 <StaticList/> 中一样生成初始列表
// 但这次我们将 ID 与信号一起包含在内
// 请参阅下文 add_counter 中关于 ArcRwSignal 的注释
// we generate an initial list as in <StaticList/>
// but this time we include the ID along with the signal
// see NOTE in add_counter below re: ArcRwSignal
let initial_counters = (0..initial_length)
.map(|id| (id, ArcRwSignal::new(id + 1)))
.collect::<Vec<_>>();
// 现在我们将该初始列表存储在一个信号中
// 这样,我们将能够随着时间的推移修改列表,
// 添加和删除计数器,它将响应式地发生变化
// now we store that initial list in a signal
// this way, we'll be able to modify the list over time,
// adding and removing counters, and it will change reactively
let (counters, set_counters) = signal(initial_counters);
let add_counter = move |_| {
// 为新计数器创建一个信号
// 我们在这里使用 ArcRwSignal,而不是 RwSignal
// ArcRwSignal 是一种引用计数类型,而不是我们到目前为止使用的
// 竞技场分配(arena-allocated)的信号类型。
// 当我们创建像这样的信号集合时,使用 ArcRwSignal
// 允许每个信号在其行被删除时被释放。
// create a signal for the new counter
// we use ArcRwSignal here, instead of RwSignal
// ArcRwSignal is a reference-counted type, rather than the arena-allocated
// signal types we've been using so far.
// When we're creating a collection of signals like this, using ArcRwSignal
// allows each signal to be deallocated when its row is removed.
let sig = ArcRwSignal::new(next_counter_id + 1);
// 将此计数器添加到计数器列表中
// add this counter to the list of counters
set_counters.update(move |counters| {
// 既然 `.update()` 给了我们 `&mut T`
// 我们就可以直接使用正常的 Vec 方法如 `push`
// since `.update()` gives us `&mut T`
// we can just use normal Vec methods like `push`
counters.push((next_counter_id, sig))
});
// 递增 ID 以使其始终保持唯一
// increment the ID so it's always unique
next_counter_id += 1;
};
view! {
<div>
<button on:click=add_counter>
"Add Counter"
</button>
<ul>
// <For/> 组件在这里至关重要
// 这允许高效的有键列表渲染
// The <For/> component is central here
// This allows for efficient, key list rendering
<For
// `each` 接收任何返回迭代器的函数
// 这通常应该是一个信号或派生信号
// 如果它不是响应式的,直接渲染 Vec<_> 而不是 <For/>
// `each` takes any function that returns an iterator
// this should usually be a signal or derived signal
// if it's not reactive, just render a Vec<_> instead of <For/>
each=move || counters.get()
// 键应该是每行唯一且稳定的
// 使用索引通常是个坏主意,除非你的列表
// 只会增长,因为在列表内部移动项目
// 意味着它们的索引会改变,它们都会重新渲染
// the key should be unique and stable for each row
// using an index is usually a bad idea, unless your list
// can only grow, because moving items around inside the list
// means their indices will change and they will all rerender
key=|counter| counter.0
// `children` 接收来自 `each` 迭代器的每个项目
// 并返回一个视图
// `children` receives each item from your `each` iterator
// and returns a view
children=move |(id, count)| {
// 我们可以将 ArcRwSignal 转换为可复制(Copy)的 RwSignal
// 以便在将其移动到视图中时获得更好的开发体验(DX)
// we can convert our ArcRwSignal to a Copy-able RwSignal
// for nicer DX when moving it into the view
let count = RwSignal::from(count);
view! {
<li>
<button
on:click=move |_| *count.write() += 1
>
{count}
</button>
<button
on:click=move |_| {
set_counters
.write()
.retain(|(counter_id, _)| {
counter_id != &id
});
}
>
"Remove"
</button>
</li>
}
}
/>
</ul>
</div>
}
}
fn main() {
leptos::mount::mount_to_body(App)
}
在迭代时使用 <ForEnumerate/> 访问索引
Accessing an index while iterating with <ForEnumerate/>
对于需要在迭代时访问实时索引的情况,Leptos 提供了 <ForEnumerate/> 组件。
For the cases where you need to access the real-time index while iterating, Leptos provides a <ForEnumerate/> component.
其 props 与 <For/> 组件完全相同,但在渲染 children 时,它额外提供了一个 ReadSignal<usize> 参数作为索引:
The props are identical to the <For/> component, but when rendering children it additionally provides a ReadSignal<usize> parameter as the index:
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
struct Counter {
id: usize,
count: RwSignal<i32>
}
<ForEnumerate
each=move || counters.get() // 与 <For/> 相同
key=|counter| counter.id // 与 <For/> 相同
// 提供索引信号和子项目 T
// Provides the index as a signal and the child T
children={move |index: ReadSignal<usize>, counter: Counter| {
view! {
<button>{move || index.get()} ". Value: " {move || counter.count.get()}</button>
}
}}
/>
或者它也可以与更方便的 let 语法一起使用:
or it could also be used with the more convenient let syntax:
<ForEnumerate
each=move || counters.get() // 与 <For/> 相同
key=|counter| counter.id // 与 <For/> 相同
let(idx, counter) // let 语法
>
<button>{move || idx.get()} ". Value: " {move || counter.count.get()}</button>
</ ForEnumerate>