列表渲染

在开发中,我们经常需要根据数组数据渲染一组相似的元素。Vitarx 提供了 For 组件来高效渲染动态列表。

基本用法

For 组件接收 each 属性作为数据源,children 作为渲染函数:

tsx
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>(响应式引用)
tsx
<For each={fruits}>
  {(fruit, index) => (
    <li>
      第 {index} 项: {fruit}
    </li>
  )}
</For>

INFO

indexRef<number> 类型,在模板中会自动解包显示数值,在 JavaScript 代码中需要通过 index.value 访问。

key 属性

key 用于标识列表项的唯一性,帮助框架高效地更新列表。强烈建议始终提供 key

字符串形式

当数据是对象数组时,传入唯一标识的属性名:

tsx
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 时,可以传入函数:

tsx
<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 包装的数组),数据变化时列表会自动更新:

tsx
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')

完整示例

下面是一个功能完整的待办事项列表:

tsx
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) => {
    todos.value = todos.value.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo))
  }

  const removeTodo = (id: number) => {
    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 参考

tsx
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>

下一步

  • 组件基础 — 深入学习组件的生命周期和高级用法