Chapter 06

内置指令:{#if} {#each} {#await} {#key}

掌握 Svelte 模板指令系统,构建动态、异步、高性能的用户界面

{#if} 条件渲染

基本条件渲染

{#if} 是 Svelte 的条件渲染指令,根据条件决定是否将元素添加到 DOM(而不仅仅是隐藏)。这意味着条件为 false 时,元素及其子组件会完全销毁;条件再次为 true 时重新创建。

<script lang="ts">
  let status = $state<'loading' | 'success' | 'error'>('loading');
  let isLoggedIn = $state(false);
  let score = $state(75);
</script>

<!-- 基础 if/else -->
{#if isLoggedIn}
  <p>欢迎回来!</p>
{:else}
  <a href="/login">请登录</a>
{/if}

<!-- if/else if/else -->
{#if status === 'loading'}
  <div class="spinner">加载中...</div>
{:else if status === 'error'}
  <div class="error">加载失败,请重试</div>
{:else}
  <div class="content">内容已加载</div>
{/if}

<!-- 嵌套条件 -->
{#if score >= 60}
  {#if score >= 90}
    <span>优秀</span>
  {:else if score >= 75}
    <span>良好</span>
  {:else}
    <span>及格</span>
  {/if}
{:else}
  <span>不及格</span>
{/if}
{#if} vs CSS display:none

{#if} 会完全从 DOM 中移除元素(适合有副作用的组件或不常显示的内容)。若你只需要视觉隐藏而保留 DOM 节点(如保留表单值),使用 CSS style:display={visible ? 'block' : 'none'}

{#each} 列表渲染

基本列表渲染

<script lang="ts">
  interface Product {
    id: number;
    name: string;
    price: number;
    inStock: boolean;
  }

  let products = $state<Product[]>([
    { id: 1, name: '苹果', price: 5.99, inStock: true },
    { id: 2, name: '香蕉', price: 3.49, inStock: true },
    { id: 3, name: '草莓', price: 12.99, inStock: false }
  ]);
</script>

<!-- 基础列表 -->
<ul>
  {#each products as product}
    <li>{product.name} - ¥{product.price}</li>
  {/each}
</ul>

<!-- 带索引 -->
{#each products as product, index}
  <div>{index + 1}. {product.name}</div>
{/each}

<!-- 带 key(性能优化,用于列表增删排序)-->
{#each products as product (product.id)}
  <div class:out-of-stock={!product.inStock}>
    {product.name}
    {#if !product.inStock}<span>缺货</span>{/if}
  </div>
{/each}

<!-- 空列表处理 -->
{#each products as product (product.id)}
  <div>{product.name}</div>
{:else}
  <p>暂无商品</p>
{/each}

{#await} 异步数据加载

处理 Promise

{#await} 指令让你可以在模板中直接处理 Promise,自动显示加载状态、成功结果和错误信息,无需手动管理多个状态变量:

<script lang="ts">
  interface User {
    id: number;
    name: string;
    email: string;
  }

  async function fetchUser(id: number): Promise<User> {
    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
    if (!res.ok) throw new Error('用户不存在');
    return res.json();
  }

  let userId = $state(1);
  // 响应式 promise:userId 变化时自动重新请求
  let userPromise = $derived(fetchUser(userId));
</script>

<!-- 完整的三段式处理 -->
{#await userPromise}
  <div class="skeleton">加载用户信息...</div>
{:then user}
  <div class="user-card">
    <h2>{user.name}</h2>
    <p>{user.email}</p>
  </div>
{:catch error}
  <div class="error">错误:{error.message}</div>
{/await}

<!-- 简短形式:只关心成功结果(不处理加载/错误状态)-->
{#await fetchUser(1) then user}
  <p>{user.name}</p>
{/await}

<input type="number" bind:value={userId} min="1" max="10" />

{#key} 强制重渲染

强制销毁和重建组件

{#key} 用于在表达式变化时强制销毁并重建其内部的 DOM 节点。这在以下场景非常有用:触发过渡动画、重置组件内部状态、强制重新执行组件的初始化逻辑:

<script lang="ts">
  import { fade } from 'svelte/transition';
  let currentRoute = $state('home');
  let resetKey = $state(0);
</script>

<!-- 路由切换时触发过渡动画 -->
{#key currentRoute}
  <div transition:fade={{ duration: 200 }}>
    {#if currentRoute === 'home'}
      <h1>首页内容</h1>
    {:else if currentRoute === 'about'}
      <h1>关于我们</h1>
    {/if}
  </div>
{/key}

<!-- 强制重置表单组件 -->
{#key resetKey}
  <MyComplexForm />  <!-- key 变化时完全重新创建 -->
{/key}
<button onclick={() => resetKey++}>重置表单</button>

{@const} 局部常量

<!-- 在模板中声明局部常量,避免重复计算 -->
{#each products as product (product.id)}
  {@const discounted = product.price * 0.8}
  {@const isAvailable = product.inStock && product.quantity > 0}
  {@const label = isAvailable ? '立即购买' : '缺货中'}

  <div class:available={isAvailable}>
    <p>{product.name}</p>
    <p>优惠价:¥{discounted.toFixed(2)}</p>
    <button disabled={!isAvailable}>{label}</button>
  </div>
{/each}
{@const} 的使用范围

{@const} 只能在 {#if}{#each}{#await}{#snippet} 等块的直接子级中使用,不能在组件顶层使用(顶层用 $derived() 代替)。

{#snippet} 在模板内复用片段

在同一个组件内,{#snippet} 可以定义可复用的模板片段,避免重复的 HTML 结构。

<script lang="ts">
  interface Product {
    id: number; name: string; price: number; stock: number;
  }
  let products = $state<Product[]>([
    { id: 1, name: 'MacBook Pro', price: 14999, stock: 5 },
    { id: 2, name: 'iPhone 16', price: 7999, stock: 0 }
  ]);
</script>

<!-- 定义可复用的产品卡片 snippet -->
{#snippet productCard(product: Product)}
  <div class="card" class:out-of-stock={product.stock === 0}>
    <h3>{product.name}</h3>
    <p>¥{product.price.toLocaleString()}</p>
    {#if product.stock > 0}
      <span>库存:{product.stock}</span>
    {:else}
      <span class="badge">缺货</span>
    {/if}
  </div>
{/snippet}

<!-- 在多处复用同一个 snippet -->
<section>
  <h2>今日推荐</h2>
  {@render productCard(products[0])}
</section>

<section>
  <h2>全部商品</h2>
  {#each products as p (p.id)}
    {@render productCard(p)}
  {/each}
</section>

高级 {#each} 模式

<script lang="ts">
  let matrix = $state([[1, 2, 3], [4, 5, 6], [7, 8, 9]]);
  interface User { name: string; role: string; }
  let users = $state<User[]>([]);
</script>

<!-- 嵌套 {#each}:渲染矩阵/表格 -->
<table>
  {#each matrix as row, rowIndex}
    <tr>
      {#each row as cell, colIndex}
        <td class:diagonal={rowIndex === colIndex}>{cell}</td>
      {/each}
    </tr>
  {/each}
</table>

<!-- {:else} 块:列表为空时的占位内容 -->
{#each users as user (user.name)}
  <div>{user.name} - {user.role}</div>
{:else}
  <p class="empty-state">暂无用户数据</p>
{/each}

<!-- 解构:直接解构对象属性,减少重复引用 -->
{#each users as { name, role } (name)}
  <div>{name} — {role}</div>
{/each}

Svelte 指令的工作原理

Svelte 编译器如何处理模板指令

Svelte 是一个编译时框架,理解编译器如何将模板指令转化为原生 DOM 操作,能帮助你优化性能并避免常见误区:

{#if} 的编译结果
编译器将 {#if condition} 转化为条件 DOM 创建/销毁代码:条件为 true 时插入 DOM 节点,false 时移除。与 CSS display:none 不同,DOM 节点被物理移除,组件的 onMount/onDestroy 生命周期都会被触发。这对动画(transition:)很重要——节点进入/退出 DOM 时触发 transition。
{#each} 的 key 参数作用
{#each items as item (item.id)} 中的 (item.id) 是 key——告诉 Svelte 如何将旧节点与新节点匹配。没有 key 时,Svelte 按位置匹配(第1个匹配第1个),列表重排时所有节点都要重新渲染。有 key 时,Svelte 通过 id 找到对应的 DOM 节点并复用,只移动/更新必要的节点,性能显著更好。
{#await} 的三阶段
{#await promise}(加载中)→ {:then value}(成功)→ {:catch error}(失败)。Svelte 编译器生成状态机代码,监听 Promise 状态变化并更新 DOM。如果 promise 在组件销毁前已 resolved,:then 会立即显示,没有加载态闪烁。
{#key} 强制重建的场景
{#key value} 当 value 变化时强制销毁并重建内部 DOM。主要用途:触发 transition/animation(路由切换动画)、强制重置有内部状态的组件(如清空表单填写状态)。代价是完整的 DOM 重建,应谨慎使用,不要用于频繁变化的值。

指令性能对比

显示/隐藏元素的三种方式对比:

方式1:{#if condition}
  DOM 中完全移除/插入节点
  优点:节点不存在时无内存占用
  缺点:插入时重新执行 onMount,有动画时需要 transition 指令
  适用:切换频率低,节点较重(如模态框、复杂表单)

方式2:class:hidden={!condition} (CSS hidden)
  节点始终在 DOM 中,通过 CSS 控制可见性
  优点:切换瞬时,无生命周期开销
  缺点:不可见时 DOM 节点仍占内存
  适用:切换频率高,节点较轻(如 tooltip、下拉菜单)

方式3:style:display={condition ? 'block' : 'none'}
  等同于方式2,但通过内联样式控制
  适用:同方式2,在动态计算显示逻辑时更灵活
第6章小结