For

For 是 Vitarx 的列表渲染组件,用于根据数组数据动态渲染列表。它支持 key 优化、列表动画钩子,以及高效的差量更新。

基本用法

tsx
import { For, reactive } from 'vitarx'

function App() {
  const items = reactive(['苹果', '香蕉', '橘子'])

  return (
    <ul>
      <For each={items}>
        {(item, index) => (
          <li>
            {index.value + 1}. {item}
          </li>
        )}
      </For>
    </ul>
  )
}

children 是一个渲染函数,接收两个参数:

  • item — 当前列表项的数据
  • index — 当前项的索引(Ref<number> 类型,响应式)

注意:index 是响应式引用,需要通过 .value 访问实际值。

属性

属性类型必填说明
eachreadonly T[]数据源数组
children(item: T, index: Ref<number>) => RenderChild列表项渲染函数
keykeyof T | ((item: T) => any)列表项唯一标识,强烈建议提供
onLeave(view: View, done: () => void) => void列表项离开动画钩子
onEnter(view: View) => void列表项进入动画钩子
onBeforeUpdate(children: IterableIterator<View>) => void列表更新前回调
onAfterUpdate(children: IterableIterator<View>) => void列表更新后回调

key 属性

key 用于标识列表项的唯一性,帮助框架高效地复用和移动 DOM 节点。虽然 key 不是必填的,但强烈建议提供,否则框架会发出警告。

key 支持两种形式:

字符串属性名

当列表项是对象,且某个属性可以作为唯一标识时,直接传属性名:

tsx
import { For, reactive } from 'vitarx'

interface User {
  id: number
  name: string
}

function UserList() {
  const users = reactive<User[]>([
    { id: 1, name: '张三' },
    { id: 2, name: '李四' },
    { id: 3, name: '王五' }
  ])

  return (
    <For each={users} key="id">
      {(user) => <div>{user.name}</div>}
    </For>
  )
}

函数形式

当需要更灵活的 key 生成逻辑时,可以传函数:

tsx
<For each={users} key={(user) => user.id}>
  {(user) => <div>{user.name}</div>}
</For>

key 重复检测:框架会自动检测同一列表中是否存在重复的 key,并在开发模式下发出警告。

列表动画钩子

For 提供了四个动画钩子,让你可以在列表项增删时添加动画效果。

onLeave — 离开动画

当列表项被移除时触发。你需要手动调用 done() 来通知框架动画完成,框架才会真正移除 DOM 元素。

tsx
<For
  each={items}
  key="id"
  onLeave={(view, done) => {
    // 添加淡出动画
    const el = view.node as HTMLElement
    el.style.transition = 'opacity 0.3s'
    el.style.opacity = '0'
    // 300ms 后通知框架移除元素
    setTimeout(done, 300)
  }}
>
  {(item) => <div>{item.name}</div>}
</For>

onEnter — 进入动画

当新列表项被添加到 DOM 后触发。

tsx
<For
  each={items}
  key="id"
  onEnter={(view) => {
    // 添加淡入动画
    const el = view.node as HTMLElement
    el.style.opacity = '0'
    requestAnimationFrame(() => {
      el.style.transition = 'opacity 0.3s'
      el.style.opacity = '1'
    })
  }}
>
  {(item) => <div>{item.name}</div>}
</For>

onBeforeUpdate / onAfterUpdate

在列表更新前后触发,可以用来记录位置、执行布局计算等。

tsx
<For
  each={items}
  key="id"
  onBeforeUpdate={(children) => {
    // 更新前:记录当前元素位置
    console.log('列表即将更新')
  }}
  onAfterUpdate={(children) => {
    // 更新后:重新计算布局
    console.log('列表更新完成')
  }}
>
  {(item) => <div>{item.name}</div>}
</For>

完整示例

下面是一个包含增删操作和动画效果的完整示例:

tsx
import { For, reactive, ref } from 'vitarx'

interface TodoItem {
  id: number
  text: string
}

function TodoApp() {
  let nextId = 4
  const todos = reactive<TodoItem[]>([
    { id: 1, text: '学习 Vitarx' },
    { id: 2, text: '写一个项目' },
    { id: 3, text: '部署上线' }
  ])
  const inputText = ref('')

  /** 添加待办 */
  const addTodo = () => {
    const text = inputText.value.trim()
    if (!text) return
    todos.push({ id: nextId++, text })
    inputText.value = ''
  }

  /** 删除待办 */
  const removeTodo = (id: number) => {
    const index = todos.findIndex((item) => item.id === id)
    if (index !== -1) todos.splice(index, 1)
  }

  return (
    <div>
      <h2>待办事项</h2>
      <div>
        <input
          value={inputText}
          onInput={(e) => {
            inputText.value = (e.target as HTMLInputElement).value
          }}
          onKeyDown={(e) => {
            if (e.key === 'Enter') addTodo()
          }}
        />
        <button onClick={addTodo}>添加</button>
      </div>
      <ul>
        <For
          each={todos}
          key="id"
          onEnter={(view) => {
            const el = view.node as HTMLElement
            el.style.opacity = '0'
            el.style.transform = 'translateX(-20px)'
            requestAnimationFrame(() => {
              el.style.transition = 'all 0.3s ease'
              el.style.opacity = '1'
              el.style.transform = 'translateX(0)'
            })
          }}
          onLeave={(view, done) => {
            const el = view.node as HTMLElement
            el.style.transition = 'all 0.3s ease'
            el.style.opacity = '0'
            el.style.transform = 'translateX(20px)'
            setTimeout(done, 300)
          }}
        >
          {(todo) => (
            <li>
              <span>{todo.text}</span>
              <button onClick={() => removeTodo(todo.id)}>删除</button>
            </li>
          )}
        </For>
      </ul>
    </div>
  )
}

下一步