列表渲染
在开发中,我们经常需要根据数组数据渲染一组相似的元素。Vitarx 提供了 For 组件来高效渲染动态列表。
基本用法
For 组件接收 each 属性作为数据源,children 作为渲染函数:
import { For, createApp } from 'vitarx'
function App() {
const fruits = ['苹果', '香蕉', '橘子']
return (
<ul>
<For each={fruits}>{(fruit) => <li>{fruit}</li>}</For>
</ul>
)
}
createApp(App).mount('#app')渲染函数接收两个参数:
- item:当前项的数据
- index:当前项的索引,是一个
Ref<number>(响应式引用)
<For each={fruits}>
{(fruit, index) => (
<li>
第 {index} 项: {fruit}
</li>
)}
</For>INFO
index 是 Ref<number> 类型,在模板中会自动解包显示数值,在 JavaScript 代码中需要通过 index.value 访问。
key 属性
key 用于标识列表项的唯一性,帮助框架高效地更新列表。强烈建议始终提供 key:
字符串形式
当数据是对象数组时,传入唯一标识的属性名:
import { For, ref, createApp } from 'vitarx'
function App() {
const users = ref([
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' }
])
return (
<ul>
<For each={users.value} key="id">
{(user) => <li>{user.name}</li>}
</For>
</ul>
)
}
createApp(App).mount('#app')函数形式
当你需要更灵活地生成 key 时,可以传入函数:
<For each={users.value} key={(user) => user.id}>
{(user) => <li>{user.name}</li>}
</For>为什么需要 key
key 的作用是帮助框架识别哪些项发生了变化(添加、删除、移动)。没有 key 时,框架无法高效地复用已有 DOM,可能导致不必要的重建和状态丢失。
- 有 key:框架通过 key 匹配新旧列表项,只更新变化的项,保持未变化项的 DOM 和状态
- 没有 key:框架无法精确匹配,可能重建本可以复用的 DOM,性能较差
WARNING
key 的值必须在列表中唯一。重复的 key 会导致渲染异常。不要使用数组索引作为 key,因为当列表项增删时索引会变化,失去 key 的意义。
响应式列表
当列表数据是响应式的(如 ref 包装的数组),数据变化时列表会自动更新:
import { For, ref, createApp } from 'vitarx'
function App() {
const items = ref([
{ id: 1, text: '学习 Vitarx' },
{ id: 2, text: '编写组件' },
{ id: 3, text: '构建应用' }
])
let nextId = 4
const addItem = () => {
items.value = [...items.value, { id: nextId++, text: `新任务 #123;nextId - 1}` }]
}
const removeItem = (id: number) => {
items.value = items.value.filter((item) => item.id !== id)
}
return (
<div style={{ padding: '20px', maxWidth: '400px', margin: '0 auto' }}>
<h2>待办列表</h2>
<button onClick={addItem} style={{ marginBottom: '12px' }}>
添加任务
</button>
<ul style={{ paddingLeft: '20px' }}>
<For each={items.value} key="id">
{(item) => (
<li style={{ margin: '8px 0' }}>
{item.text}
<button onClick={() => removeItem(item.id)} style={{ marginLeft: '8px' }}>
删除
</button>
</li>
)}
</For>
</ul>
</div>
)
}
createApp(App).mount('#app')列表数据更新
vitarx 的列表更新模型是「行内信号响应式 + 数据项快照」:
children渲染函数只在行创建时执行一次,行内容通过 JSX 表达式读取item属性建立信号依赖;key决定行的身份——同 key 的行会被复用(保留 DOM 节点与行内状态,如输入焦点),children不会重新执行。
因此更新列表数据有三条通道,按场景选择:
原地修改(推荐)
行数据变化时,直接修改响应式数组中该行对象的属性,行内信号会精确更新对应的 DOM:
const todos = ref<Todo[]>([...])
// 修改某行:原地改属性,仅该行的对应表达式更新
const toggleTodo = (id: number) => {
const todo = todos.value.find((t) => t.id === id)
if (todo) todo.done = !todo.done
}syncList 同步全量数据
新增于 4.1.0 版本
从服务端拉取全量新数据时,用 syncList 把新数据按 key 回写进既有数组——不替换数组引用,同 key 行复用并元素级更新:
import { syncList } from 'vitarx'
async function refresh() {
const fresh = await fetchTodos() // 全新对象的全量数据
syncList(todos.value, fresh, 'id')
}key 携带可变内容
当「内容变化即意味着新身份」(如版本化数据、缓存失效场景)时,把可变内容纳入 key,内容变化即精确重建该行:
<For each={articles} key={(a) => `#123;a.id}:#123;a.version}`}>
{(article) => <ArticleCard data={article} />}
</For>WARNING
// ❌ 直接替换数组,全部是新对象引用
const fresh = await fetchTodos()
todos.value = fresh同 key 的行会被复用,但 children 不会重新执行——行内容不会自动更新,且开发模式下会输出聚合警告(提示使用上述三种姿势)。若确实需要消化全量新数据,请使用 syncList。
增删与重排则不受此限制——spread / filter / splice 等数组操作只要保持未变化项的对象引用稳定,For 就会按 key 复用既有行:
// ✅ 新数组,但旧项引用保持稳定:旧行全部复用,仅新行构建
todos.value = [...todos.value, { id: nextId++, text: '新任务' }]
todos.value = todos.value.filter((t) => t.id !== id)完整示例
下面是一个功能完整的待办事项列表:
import { For, ref, createApp } from 'vitarx'
interface Todo {
id: number
text: string
done: boolean
}
function App() {
const todos = ref<Todo[]>([
{ id: 1, text: '学习 Vitarx', done: false },
{ id: 2, text: '编写组件', done: false },
{ id: 3, text: '构建应用', done: true }
])
const newText = ref('')
let nextId = 4
const addTodo = () => {
const text = newText.value.trim()
if (!text) return
todos.value = [...todos.value, { id: nextId++, text, done: false }]
newText.value = ''
}
const toggleTodo = (id: number) => {
// 原地修改:直接改该行对象的属性,行内信号精确更新(见「列表数据更新」)
const todo = todos.value.find((t) => t.id === id)
if (todo) todo.done = !todo.done
}
const removeTodo = (id: number) => {
// filter 产生新数组,但未变化项的对象引用保持稳定,行按 key 复用
todos.value = todos.value.filter((todo) => todo.id !== id)
}
return (
<div style={{ padding: '20px', maxWidth: '400px', margin: '0 auto' }}>
<h2>待办事项</h2>
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
<input
value={newText}
onInput={(e) => {
newText.value = (e.target as HTMLInputElement).value
}}
onKeyDown={(e) => {
if (e.key === 'Enter') addTodo()
}}
placeholder="输入新任务"
style={{ flex: 1, padding: '8px' }}
/>
<button onClick={addTodo} style={{ padding: '8px 16px' }}>
添加
</button>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
<For each={todos.value} key="id">
{(todo) => (
<li
style={{
display: 'flex',
alignItems: 'center',
padding: '8px',
marginBottom: '4px',
background: '#f5f5f5',
borderRadius: '4px'
}}
>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
style={{ marginRight: '8px' }}
/>
<span
style={{
flex: 1,
textDecoration: todo.done ? 'line-through' : 'none',
color: todo.done ? '#999' : '#333'
}}
>
{todo.text}
</span>
<button
onClick={() => removeTodo(todo.id)}
style={{ color: '#e74c3c', cursor: 'pointer' }}
>
删除
</button>
</li>
)}
</For>
</ul>
<p style={{ color: '#999', fontSize: '14px', marginTop: '12px' }}>
共 {todos.value.length} 项,已完成 {todos.value.filter((t) => t.done).length} 项
</p>
</div>
)
}
createApp(App).mount('#app')API 参考
interface ForProps<T> {
/** 列表数据源 */
each: readonly T[]
/** 列表项渲染函数 */
children: (item: T, index: Ref<number>) => RenderChild
/** 唯一标识——属性名或函数 */
key?: keyof T | ((item: T) => any)
}
// 使用方式
;<For each={list} key="id">
{(item, index) => <div>{item.name}</div>}
</For>下一步
- 组件基础 — 深入学习组件的生命周期和高级用法