MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Svelte列表渲染:each块的使用技巧

2021-11-011.6k 阅读

Svelte列表渲染:each块的使用技巧

在前端开发中,列表渲染是一个常见的需求。无论是展示用户列表、商品清单还是其他任何形式的集合数据,高效且灵活地渲染列表至关重要。Svelte作为一种现代的前端框架,提供了强大且直观的方式来处理列表渲染,这主要通过 each 块来实现。

基本的each块用法

在Svelte中,each 块用于迭代数组并为数组中的每个元素创建一个DOM片段。其基本语法如下:

{#each array as item}
  <!-- 这里可以使用item来渲染列表项 -->
  <p>{item}</p>
{/each}

例如,假设我们有一个包含水果名称的数组:

<script>
  const fruits = ['apple', 'banana', 'cherry'];
</script>

{#each fruits as fruit}
  <li>{fruit}</li>
{/each}

上述代码会在页面上渲染出一个无序列表,每个列表项是数组中的一个水果名称。这里的 as 关键字用于指定迭代过程中每个数组元素的别名,在这个例子中别名是 fruit,我们可以在 each 块内部使用 fruit 来访问当前迭代的数组元素。

带索引的each块

有时候,我们不仅需要访问数组中的元素,还需要知道元素的索引位置。在Svelte的 each 块中,可以通过在 as 关键字后添加第二个参数来获取索引。语法如下:

{#each array as item, index}
  <!-- 这里可以使用item和index -->
  <p>{index}: {item}</p>
{/each}

以下是一个示例,展示如何使用索引来为列表项添加序号:

<script>
  const numbers = [10, 20, 30];
</script>

{#each numbers as number, index}
  <li>{index + 1}. {number}</li>
{/each}

上述代码会渲染出一个有序列表,列表项前面的序号由索引值加1得到,这样可以为用户提供一个直观的计数效果。

each块中的key

当使用 each 块渲染列表时,如果列表中的元素发生了变化(例如添加、删除或重新排序),Svelte需要一种方式来高效地更新DOM。这就是 key 的作用。key 是一个唯一标识符,用于帮助Svelte识别列表中的每个元素。

each 块中指定 key 的语法如下:

{#each array as item (item.id)}
  <!-- 这里使用item.id作为key -->
  <div>{item.name}</div>
{/each}

假设我们有一个包含用户信息的数组,每个用户有一个唯一的 id

<script>
  const users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
    { id: 3, name: 'Charlie' }
  ];
</script>

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

通过使用 user.id 作为 key,当数组中的用户信息发生变化时,Svelte能够准确地知道哪个用户发生了改变,从而只更新对应的DOM元素,而不是重新渲染整个列表。这大大提高了渲染效率,尤其是在处理大型列表时。

动态添加和删除列表项

结合Svelte的响应式系统,我们可以轻松地实现动态添加和删除列表项的功能。

首先,让我们来看如何动态添加列表项。假设我们有一个输入框和一个按钮,用户可以在输入框中输入新的水果名称,然后点击按钮将其添加到水果列表中:

<script>
  let fruits = ['apple', 'banana', 'cherry'];
  let newFruit = '';

  const addFruit = () => {
    if (newFruit) {
      fruits = [...fruits, newFruit];
      newFruit = '';
    }
  };
</script>

<input type="text" bind:value={newFruit} placeholder="Add a new fruit">
<button on:click={addFruit}>Add Fruit</button>

{#each fruits as fruit}
  <li>{fruit}</li>
{/each}

在上述代码中,我们使用了Svelte的双向绑定 bind:value 来获取输入框的值,并将其存储在 newFruit 变量中。当用户点击按钮时,addFruit 函数被调用,它将 newFruit 的值添加到 fruits 数组中,并清空 newFruit。由于 fruits 是响应式的,Svelte会自动重新渲染 each 块,从而在页面上显示新添加的水果。

接下来,看看如何动态删除列表项。我们可以为每个列表项添加一个删除按钮,当用户点击按钮时,对应的列表项从数组中删除:

<script>
  let fruits = ['apple', 'banana', 'cherry'];

  const removeFruit = (index) => {
    fruits = fruits.filter((_, i) => i!== index);
  };
</script>

{#each fruits as fruit, index}
  <li>
    {fruit}
    <button on:click={() => removeFruit(index)}>Remove</button>
  </li>
{/each}

在这个例子中,removeFruit 函数接收要删除的列表项的索引。它使用 filter 方法创建一个新的数组,其中不包含要删除的列表项。同样,由于 fruits 是响应式的,Svelte会自动更新页面,移除对应的列表项。

嵌套each块

在实际应用中,我们可能会遇到需要渲染嵌套列表的情况。例如,一个包含多个部门,每个部门又有多个员工的组织结构。在Svelte中,可以通过嵌套 each 块来实现这种需求。

假设我们有如下的数据结构:

<script>
  const departments = [
    {
      name: 'Engineering',
      employees: [
        { name: 'Alice', role: 'Developer' },
        { name: 'Bob', role: 'Engineer' }
      ]
    },
    {
      name: 'Marketing',
      employees: [
        { name: 'Charlie', role: 'Marketer' }
      ]
    }
  ];
</script>

{#each departments as department}
  <h2>{department.name}</h2>
  <ul>
    {#each department.employees as employee}
      <li>{employee.name} - {employee.role}</li>
    {/each}
  </ul>
{/each}

上述代码中,外层的 each 块迭代 departments 数组,为每个部门渲染一个标题。内层的 each 块则迭代每个部门的 employees 数组,为每个员工渲染一个列表项。这样就实现了嵌套列表的渲染。

处理复杂数据结构的列表渲染

除了简单的数组,Svelte的 each 块还能处理更复杂的数据结构。例如,我们可能有一个对象数组,每个对象包含多个属性和子对象,甚至可能包含数组。

假设我们有一个包含书籍信息的数组,每本书籍对象包含作者、出版年份和评论数组:

<script>
  const books = [
    {
      title: 'JavaScript: The Definitive Guide',
      author: 'David Flanagan',
      year: 2011,
      reviews: [
        { rating: 4, comment: 'Great book!' },
        { rating: 5, comment: 'Highly recommended' }
      ]
    },
    {
      title: 'Learning Svelte',
      author: 'Rich Harris',
      year: 2021,
      reviews: [
        { rating: 5, comment: 'A must - read for Svelte beginners' }
      ]
    }
  ];
</script>

{#each books as book}
  <h3>{book.title}</h3>
  <p>Author: {book.author}, Year: {book.year}</p>
  <h4>Reviews</h4>
  <ul>
    {#each book.reviews as review}
      <li>Rating: {review.rating}, Comment: {review.comment}</li>
    {/each}
  </ul>
{/each}

在这个例子中,我们通过多层 each 块来处理复杂的数据结构。外层 each 块迭代书籍数组,然后在内部为每本书籍渲染标题、作者和出版年份。接着,对于每本书籍的评论数组,我们使用内层 each 块来渲染每个评论的评分和评论内容。

在each块中使用组件

Svelte组件是构建复杂用户界面的基石,在 each 块中使用组件可以极大地提高代码的复用性和可维护性。

假设我们有一个简单的 ListItem 组件,用于渲染列表项:

<!-- ListItem.svelte -->
<script>
  export let item;
</script>

<li>{item}</li>

然后在主组件中,我们可以这样使用 ListItem 组件来渲染水果列表:

<script>
  import ListItem from './ListItem.svelte';
  const fruits = ['apple', 'banana', 'cherry'];
</script>

{#each fruits as fruit}
  <ListItem {item: fruit} />
{/each}

通过将 ListItem 组件引入主组件,并在 each 块中使用它,我们将列表项的渲染逻辑封装到了 ListItem 组件中。这样,如果我们需要对列表项的样式或行为进行修改,只需要在 ListItem 组件中进行修改,而无需在主组件的 each 块中逐个修改。

如果列表项组件需要更复杂的数据和交互,我们也可以传递多个属性和事件处理函数。例如,假设 ListItem 组件支持点击事件并显示更多详细信息:

<!-- ListItem.svelte -->
<script>
  export let item;
  export let onItemClick;
  let showDetails = false;
</script>

<li on:click={onItemClick}>
  {item}
  {#if showDetails}
    <p>Some detailed information about {item}</p>
  {/if}
</script>

在主组件中:

<script>
  import ListItem from './ListItem.svelte';
  const fruits = ['apple', 'banana', 'cherry'];

  const handleItemClick = (item) => {
    console.log(`Clicked on ${item}`);
  };
</script>

{#each fruits as fruit}
  <ListItem {item} {onItemClick: () => handleItemClick(fruit)} />
{/each}

这里,我们通过 onItemClick 属性将点击事件处理函数传递给 ListItem 组件,并且在组件内部根据点击状态显示或隐藏详细信息。

each块与过渡效果

Svelte提供了丰富的过渡效果,可以应用在 each 块渲染的列表项上,为用户带来更流畅和吸引人的交互体验。

例如,我们可以为列表项添加淡入淡出的过渡效果。首先,在Svelte组件中导入过渡效果:

<script>
  import fade from'svelte/transition';
  const fruits = ['apple', 'banana', 'cherry'];
</script>

{#each fruits as fruit}
  <li transition:fade>{{fruit}}</li>
{/each}

在上述代码中,通过 transition:fade 为每个列表项添加了淡入淡出的过渡效果。当列表项被添加或删除时,会以淡入淡出的方式显示或消失。

如果我们想要更复杂的过渡效果,比如滑动效果,可以使用 slide 过渡:

<script>
  import slide from'svelte/transition';
  const fruits = ['apple', 'banana', 'cherry'];
</script>

{#each fruits as fruit}
  <li transition:slide="{{y: 50, duration: 300}}">{{fruit}}</li>
{/each}

这里,slide 过渡的参数 y: 50 表示列表项在滑动时垂直方向移动50像素,duration: 300 表示过渡持续时间为300毫秒。这样,列表项在添加或删除时会以滑动的方式呈现,为用户提供更生动的视觉反馈。

在each块中优化性能

在处理大型列表时,性能优化至关重要。Svelte的 each 块已经通过 key 等机制提供了一定的性能优化,但我们还可以采取一些额外的措施。

一种常见的优化方法是使用 window.requestAnimationFrame 来批量更新列表。假设我们有一个函数用于向列表中添加多个项目:

<script>
  let fruits = [];

  const addMultipleFruits = () => {
    const newFruits = ['date', 'elderberry', 'fig'];
    window.requestAnimationFrame(() => {
      fruits = [...fruits, ...newFruits];
    });
  };
</script>

<button on:click={addMultipleFruits}>Add Multiple Fruits</button>

{#each fruits as fruit}
  <li>{fruit}</li>
{/each}

通过 requestAnimationFrame,我们将列表的更新操作推迟到浏览器下一次重绘之前执行,这样可以避免频繁的重绘和回流,提高性能。

另一种优化方式是使用虚拟列表。虚拟列表只渲染当前可见区域内的列表项,而不是渲染整个列表。虽然Svelte本身没有内置虚拟列表的实现,但可以借助第三方库如 svelte-virtual-list 来实现。

首先安装 svelte-virtual-list

npm install svelte-virtual-list

然后在组件中使用:

<script>
  import { VirtualList } from'svelte-virtual-list';
  const largeList = Array.from({ length: 1000 }, (_, i) => `Item ${i + 1}`);

  const renderItem = ({ index, item }) => {
    return <li>{item}</li>;
  };
</script>

<VirtualList {items: largeList} {renderItem} height={200} itemSize={30} />

在上述代码中,VirtualList 组件只渲染当前可见区域内的列表项,height 属性指定了列表的可见高度,itemSize 属性指定了每个列表项的高度。通过这种方式,即使列表非常大,也能保持良好的性能。

处理异步数据的列表渲染

在实际应用中,列表数据往往是通过异步请求获取的。Svelte提供了简洁的方式来处理异步数据的列表渲染。

假设我们使用 fetch 来获取用户列表:

<script>
  let users = [];
  let isLoading = false;

  const fetchUsers = async () => {
    isLoading = true;
    try {
      const response = await fetch('https://example.com/api/users');
      const data = await response.json();
      users = data;
    } catch (error) {
      console.error('Error fetching users:', error);
    } finally {
      isLoading = false;
    }
  };

  fetchUsers();
</script>

{#if isLoading}
  <p>Loading users...</p>
{:else if users.length === 0}
  <p>No users found.</p>
{:else}
  {#each users as user}
    <li>{user.name}</li>
  {/each}
{/if}

在上述代码中,我们首先定义了 users 数组来存储获取到的用户数据,isLoading 变量用于表示数据是否正在加载。fetchUsers 函数通过 fetch 发起异步请求,在请求过程中设置 isLoadingtrue,请求成功后将数据赋值给 users,请求失败则在控制台打印错误信息。最后,通过 if 块来根据不同的状态渲染相应的内容:加载时显示加载提示,没有数据时显示提示信息,有数据时则通过 each 块渲染用户列表。

结合响应式数据更新列表

Svelte的响应式系统与 each 块紧密结合,使得列表能够根据数据的变化自动更新。

例如,假设我们有一个开关按钮,用于切换列表的显示格式。列表数据是一个包含数字的数组,我们可以根据开关状态决定是显示数字本身还是数字的平方:

<script>
  let numbers = [1, 2, 3, 4, 5];
  let showSquares = false;

  const toggleSquares = () => {
    showSquares =!showSquares;
  };
</script>

<button on:click={toggleSquares}>
  {showSquares? 'Show Numbers' : 'Show Squares'}
</button>

{#each numbers as number}
  <li>{showSquares? number * number : number}</li>
{/each}

在这个例子中,showSquares 是一个响应式变量。当用户点击按钮时,toggleSquares 函数改变 showSquares 的值,由于 each 块依赖于 showSquaresnumbers,Svelte会自动重新渲染 each 块,从而根据新的 showSquares 值以不同的格式显示列表项。

解决each块中的一些常见问题

在使用 each 块的过程中,可能会遇到一些常见问题。

列表项点击事件冲突

当列表项包含多个可点击元素时,可能会出现点击事件冲突的情况。例如,一个列表项中有一个按钮和列表项本身都有点击事件。为了避免冲突,可以使用事件委托。

假设我们有如下代码:

<script>
  const items = ['Item 1', 'Item 2', 'Item 3'];

  const handleItemClick = (item) => {
    console.log(`Clicked on item: ${item}`);
  };

  const handleButtonClick = (item) => {
    console.log(`Clicked on button of item: ${item}`);
  };
</script>

{#each items as item}
  <li on:click={() => handleItemClick(item)}>
    {item}
    <button on:click={(e) => {
      e.stopPropagation();
      handleButtonClick(item);
    }}>Click Me</button>
  </li>
{/each}

在上述代码中,按钮的点击事件处理函数中调用了 e.stopPropagation(),这会阻止点击事件向上冒泡到列表项的点击事件处理函数,从而避免了事件冲突。

动态更新列表时的闪烁问题

在动态添加或删除列表项时,有时会出现列表闪烁的问题。这通常是由于没有正确使用 key 导致的。确保为每个列表项提供唯一且稳定的 key 可以解决这个问题。

例如,在前面动态添加和删除水果的例子中,如果没有为 each 块中的列表项指定 key,当添加或删除水果时,可能会出现列表闪烁的现象。通过为每个水果添加一个唯一的 id 并作为 key,就可以避免这个问题:

<script>
  let fruits = [
    { id: 1, name: 'apple' },
    { id: 2, name: 'banana' },
    { id: 3, name: 'cherry' }
  ];
  let newFruit = '';

  const addFruit = () => {
    if (newFruit) {
      const newId = fruits.length + 1;
      fruits = [...fruits, { id: newId, name: newFruit }];
      newFruit = '';
    }
  };

  const removeFruit = (id) => {
    fruits = fruits.filter((fruit) => fruit.id!== id);
  };
</script>

<input type="text" bind:value={newFruit} placeholder="Add a new fruit">
<button on:click={addFruit}>Add Fruit</button>

{#each fruits as fruit (fruit.id)}
  <li>
    {fruit.name}
    <button on:click={() => removeFruit(fruit.id)}>Remove</button>
  </li>
{/each}

通过为每个水果对象添加唯一的 id 并在 each 块中使用 fruit.id 作为 key,Svelte能够准确地跟踪每个列表项的变化,从而避免闪烁问题。

通过深入理解和掌握Svelte中 each 块的这些使用技巧,前端开发者可以更加高效、灵活地构建出功能丰富且性能优良的列表组件,为用户带来更好的体验。无论是简单的列表展示,还是复杂的动态交互列表,each 块都提供了强大的支持。