列表渲染 #

一、基本语法 #

1.1 each 块 #

svelte
<script>
  let items = ['Apple', 'Banana', 'Orange'];
</script>

<ul>
  {#each items as item}
    <li>{item}</li>
  {/each}
</ul>

1.2 带索引的 each #

svelte
<script>
  let items = ['Apple', 'Banana', 'Orange'];
</script>

<ul>
  {#each items as item, index}
    <li>{index + 1}. {item}</li>
  {/each}
</ul>

1.3 对象数组 #

svelte
<script>
  let users = [
    { id: 1, name: 'Alice', age: 25 },
    { id: 2, name: 'Bob', age: 30 },
    { id: 3, name: 'Charlie', age: 35 }
  ];
</script>

<ul>
  {#each users as user}
    <li>{user.name} ({user.age})</li>
  {/each}
</ul>

二、Key 的使用 #

2.1 为什么需要 Key #

text
┌─────────────────────────────────────────────────────────────┐
│                    Key 的作用                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  无 Key(按索引匹配)                                        │
│  ┌─────┐ ┌─────┐ ┌─────┐                                   │
│  │ A 0 │ │ B 1 │ │ C 2 │                                   │
│  └─────┘ └─────┘ └─────┘                                   │
│     ↓      ↓      ↓                                        │
│  删除 B 后:                                                │
│  ┌─────┐ ┌─────┐                                           │
│  │ A 0 │ │ C 1 │  ← C 移动到索引1,状态可能错乱            │
│  └─────┘ └─────┘                                           │
│                                                             │
│  有 Key(按唯一标识匹配)                                    │
│  ┌─────┐ ┌─────┐ ┌─────┐                                   │
│  │ A 1 │ │ B 2 │ │ C 3 │                                   │
│  └─────┘ └─────┘ └─────┘                                   │
│     ↓           ↓                                          │
│  删除 B 后:                                                │
│  ┌─────┐ ┌─────┐                                           │
│  │ A 1 │ │ C 3 │  ← C 保持正确,状态正确                    │
│  └─────┘ └─────┘                                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2.2 使用 Key #

svelte
<script>
  let items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' }
  ];
</script>

<ul>
  {#each items as item (item.id)}
    <li>{item.name}</li>
  {/each}
</ul>

2.3 Key 的正确使用 #

svelte
<script>
  let todos = $state([
    { id: 1, text: 'Learn Svelte', done: false },
    { id: 2, text: 'Build an app', done: false },
    { id: 3, text: 'Deploy', done: false }
  ]);
  
  function removeTodo(id) {
    todos = todos.filter(t => t.id !== id);
  }
  
  function toggleTodo(id) {
    todos = todos.map(t => 
      t.id === id ? { ...t, done: !t.done } : t
    );
  }
</script>

<ul>
  {#each todos as todo (todo.id)}
    <li class:done={todo.done}>
      <input 
        type="checkbox" 
        checked={todo.done}
        onchange={() => toggleTodo(todo.id)}
      />
      {todo.text}
      <button onclick={() => removeTodo(todo.id)}>×</button>
    </li>
  {/each}
</ul>

三、嵌套循环 #

3.1 双层嵌套 #

svelte
<script>
  let categories = [
    {
      name: 'Fruits',
      items: ['Apple', 'Banana', 'Orange']
    },
    {
      name: 'Vegetables',
      items: ['Carrot', 'Broccoli', 'Spinach']
    }
  ];
</script>

{#each categories as category}
  <div class="category">
    <h3>{category.name}</h3>
    <ul>
      {#each category.items as item}
        <li>{item}</li>
      {/each}
    </ul>
  </div>
{/each}

3.2 带索引的嵌套 #

svelte
<script>
  let matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
  ];
</script>

<table>
  {#each matrix as row, rowIndex}
    <tr>
      {#each row as cell, colIndex}
        <td>
          [{rowIndex},{colIndex}]: {cell}
        </td>
      {/each}
    </tr>
  {/each}
</table>

3.3 复杂数据结构 #

svelte
<script>
  let departments = [
    {
      id: 1,
      name: 'Engineering',
      teams: [
        { id: 1, name: 'Frontend', members: ['Alice', 'Bob'] },
        { id: 2, name: 'Backend', members: ['Charlie', 'David'] }
      ]
    },
    {
      id: 2,
      name: 'Design',
      teams: [
        { id: 3, name: 'UI/UX', members: ['Eve', 'Frank'] }
      ]
    }
  ];
</script>

{#each departments as dept (dept.id)}
  <div class="department">
    <h2>{dept.name}</h2>
    {#each dept.teams as team (team.id)}
      <div class="team">
        <h3>{team.name}</h3>
        <ul>
          {#each team.members as member}
            <li>{member}</li>
          {/each}
        </ul>
      </div>
    {/each}
  </div>
{/each}

四、列表操作 #

4.1 添加项目 #

svelte
<script>
  let items = $state([]);
  let newItem = $state('');
  
  function addItem() {
    if (newItem.trim()) {
      items = [...items, {
        id: Date.now(),
        text: newItem.trim()
      }];
      newItem = '';
    }
  }
</script>

<input 
  bind:value={newItem}
  onkeydown={(e) => e.key === 'Enter' && addItem()}
/>
<button onclick={addItem}>Add</button>

<ul>
  {#each items as item (item.id)}
    <li>{item.text}</li>
  {/each}
</ul>

4.2 删除项目 #

svelte
<script>
  let items = $state([
    { id: 1, text: 'Item 1' },
    { id: 2, text: 'Item 2' },
    { id: 3, text: 'Item 3' }
  ]);
  
  function removeItem(id) {
    items = items.filter(item => item.id !== id);
  }
</script>

<ul>
  {#each items as item (item.id)}
    <li>
      {item.text}
      <button onclick={() => removeItem(item.id)}>×</button>
    </li>
  {/each}
</ul>

4.3 更新项目 #

svelte
<script>
  let items = $state([
    { id: 1, text: 'Item 1', done: false },
    { id: 2, text: 'Item 2', done: false }
  ]);
  
  function toggleItem(id) {
    items = items.map(item =>
      item.id === id 
        ? { ...item, done: !item.done } 
        : item
    );
  }
  
  function updateText(id, text) {
    items = items.map(item =>
      item.id === id ? { ...item, text } : item
    );
  }
</script>

<ul>
  {#each items as item (item.id)}
    <li class:done={item.done}>
      <input 
        type="checkbox" 
        checked={item.done}
        onchange={() => toggleItem(item.id)}
      />
      <input 
        type="text" 
        value={item.text}
        oninput={(e) => updateText(item.id, e.target.value)}
      />
    </li>
  {/each}
</ul>

4.4 排序和过滤 #

svelte
<script>
  let items = $state([
    { id: 1, name: 'Banana', price: 2 },
    { id: 2, name: 'Apple', price: 1 },
    { id: 3, name: 'Orange', price: 3 }
  ]);
  
  let sortBy = $state('name');
  let filterText = $state('');
  
  let filteredItems = $derived(() => {
    let result = items.filter(item => 
      item.name.toLowerCase().includes(filterText.toLowerCase())
    );
    
    result.sort((a, b) => {
      if (a[sortBy] < b[sortBy]) return -1;
      if (a[sortBy] > b[sortBy]) return 1;
      return 0;
    });
    
    return result;
  });
</script>

<input bind:value={filterText} placeholder="Filter..." />

<select bind:value={sortBy}>
  <option value="name">Name</option>
  <option value="price">Price</option>
</select>

<ul>
  {#each filteredItems() as item (item.id)}
    <li>{item.name} - ${item.price}</li>
  {/each}
</ul>

五、空列表处理 #

5.1 else 块 #

svelte
<script>
  let items = $state([]);
</script>

{#each items as item (item.id)}
  <div>{item.name}</div>
{:else}
  <div class="empty">No items found</div>
{/each}

5.2 加载状态 #

svelte
<script>
  let items = $state([]);
  let loading = $state(true);
  
  async function loadItems() {
    loading = true;
    try {
      const response = await fetch('/api/items');
      items = await response.json();
    } finally {
      loading = false;
    }
  }
  
  loadItems();
</script>

{#if loading}
  <div class="loading">Loading...</div>
{:else if items.length === 0}
  <div class="empty">No items</div>
{:else}
  {#each items as item (item.id)}
    <div>{item.name}</div>
  {/each}
{/if}

六、列表过渡动画 #

6.1 简单过渡 #

svelte
<script>
  import { fade, fly, slide } from 'svelte/transition';
  
  let items = $state([]);
  
  function addItem() {
    items = [...items, { id: Date.now(), text: 'New Item' }];
  }
  
  function removeItem(id) {
    items = items.filter(item => item.id !== id);
  }
</script>

<button onclick={addItem}>Add Item</button>

<ul>
  {#each items as item (item.id)}
    <li transition:fade>
      {item.text}
      <button onclick={() => removeItem(item.id)}>×</button>
    </li>
  {/each}
</ul>

6.2 FLIP 动画 #

svelte
<script>
  import { flip } from 'svelte/animate';
  import { fade } from 'svelte/transition';
  
  let items = $state([
    { id: 1, text: 'Item 1' },
    { id: 2, text: 'Item 2' },
    { id: 3, text: 'Item 3' }
  ]);
  
  function shuffle() {
    items = items.sort(() => Math.random() - 0.5);
  }
</script>

<button onclick={shuffle}>Shuffle</button>

<ul>
  {#each items as item (item.id)}
    <li animate:flip transition:fade>
      {item.text}
    </li>
  {/each}
</ul>

七、实际应用示例 #

7.1 可排序列表 #

svelte
<script>
  let items = $state([
    { id: 1, name: 'Item 1', order: 0 },
    { id: 2, name: 'Item 2', order: 1 },
    { id: 3, name: 'Item 3', order: 2 }
  ]);
  
  function moveUp(index) {
    if (index > 0) {
      const newItems = [...items];
      [newItems[index - 1], newItems[index]] = 
        [newItems[index], newItems[index - 1]];
      items = newItems;
    }
  }
  
  function moveDown(index) {
    if (index < items.length - 1) {
      const newItems = [...items];
      [newItems[index], newItems[index + 1]] = 
        [newItems[index + 1], newItems[index]];
      items = newItems;
    }
  }
</script>

<ul>
  {#each items as item, index (item.id)}
    <li>
      {item.name}
      <button onclick={() => moveUp(index)} disabled={index === 0}>↑</button>
      <button onclick={() => moveDown(index)} disabled={index === items.length - 1}>↓</button>
    </li>
  {/each}
</ul>

7.2 分页列表 #

svelte
<script>
  let allItems = $state(Array.from({ length: 100 }, (_, i) => ({
    id: i + 1,
    name: `Item ${i + 1}`
  })));
  
  let pageSize = $state(10);
  let currentPage = $state(1);
  
  let totalPages = $derived(Math.ceil(allItems.length / pageSize));
  
  let paginatedItems = $derived(
    allItems.slice(
      (currentPage - 1) * pageSize,
      currentPage * pageSize
    )
  );
  
  function goToPage(page) {
    currentPage = Math.min(Math.max(1, page), totalPages);
  }
</script>

<ul>
  {#each paginatedItems as item (item.id)}
    <li>{item.name}</li>
  {/each}
</ul>

<div class="pagination">
  <button onclick={() => goToPage(1)} disabled={currentPage === 1}>First</button>
  <button onclick={() => goToPage(currentPage - 1)} disabled={currentPage === 1}>Prev</button>
  
  {#each Array.from({ length: totalPages }, (_, i) => i + 1) as page}
    <button 
      class:active={page === currentPage}
      onclick={() => goToPage(page)}
    >
      {page}
    </button>
  {/each}
  
  <button onclick={() => goToPage(currentPage + 1)} disabled={currentPage === totalPages}>Next</button>
  <button onclick={() => goToPage(totalPages)} disabled={currentPage === totalPages}>Last</button>
</div>

7.3 可展开列表 #

svelte
<script>
  let items = $state([
    { 
      id: 1, 
      title: 'Item 1', 
      content: 'Content for item 1',
      expanded: false 
    },
    { 
      id: 2, 
      title: 'Item 2', 
      content: 'Content for item 2',
      expanded: false 
    }
  ]);
  
  function toggle(id) {
    items = items.map(item =>
      item.id === id 
        ? { ...item, expanded: !item.expanded }
        : item
    );
  }
</script>

<div class="accordion">
  {#each items as item (item.id)}
    <div class="item">
      <div class="header" onclick={() => toggle(item.id)}>
        <span>{item.title}</span>
        <span>{item.expanded ? '−' : '+'}</span>
      </div>
      {#if item.expanded}
        <div class="content">
          {item.content}
        </div>
      {/if}
    </div>
  {/each}
</div>

八、性能优化 #

8.1 虚拟列表 #

svelte
<script>
  let allItems = Array.from({ length: 10000 }, (_, i) => ({
    id: i + 1,
    name: `Item ${i + 1}`
  }));
  
  let scrollTop = $state(0);
  let containerHeight = 400;
  let itemHeight = 40;
  let visibleCount = Math.ceil(containerHeight / itemHeight) + 2;
  
  let startIndex = $derived(Math.floor(scrollTop / itemHeight));
  let endIndex = $derived(Math.min(startIndex + visibleCount, allItems.length));
  
  let visibleItems = $derived(
    allItems.slice(startIndex, endIndex)
  );
  
  let totalHeight = allItems.length * itemHeight;
  let offsetY = $derived(startIndex * itemHeight);
  
  function handleScroll(e) {
    scrollTop = e.target.scrollTop;
  }
</script>

<div class="virtual-list" onscroll={handleScroll} style="height: {containerHeight}px">
  <div style="height: {totalHeight}px; position: relative;">
    <div style="position: absolute; top: {offsetY}px; width: 100%;">
      {#each visibleItems as item (item.id)}
        <div class="item" style="height: {itemHeight}px">
          {item.name}
        </div>
      {/each}
    </div>
  </div>
</div>

8.2 避免不必要的重新渲染 #

svelte
<script>
  let items = $state([]);
  let filter = $state('');
  
  let filteredItems = $derived(
    items.filter(item => item.name.includes(filter))
  );
</script>

{#each filteredItems as item (item.id)}
  <Item {item} />
{/each}

九、总结 #

语法 说明
{#each items as item} 基本循环
{#each items as item, index} 带索引
{#each items as item (key)} 带 Key
{:else} 空列表处理
{/each} 循环结束

列表渲染要点:

  • 使用 (key) 指定唯一标识
  • 嵌套循环时注意 key 的唯一性
  • 使用 {:else} 处理空列表
  • 大列表考虑虚拟滚动
  • 合理使用过渡动画提升体验
最后更新:2026-03-28