
当使用 #if 控制空状态显示时,Svelte 列表项的 out:fly 过渡会在最后一个元素移除时失效——因为 DOM 节点被立即销毁,来不及播放动画。解决方案是将条件渲染与过渡结束事件解耦,通过 on:outroend 延迟更新空状态标志。
当使用 `#if` 控制空状态显示时,svelte 列表项的 `out:fly` 过渡会在最后一个元素移除时失效——因为 dom 节点被立即销毁,来不及播放动画。解决方案是将条件渲染与过渡结束事件解耦,通过 `on:outroend` 延迟更新空状态标志。
在 Svelte 中,过渡(transition)的 out: 指令依赖于目标元素在 DOM 中持续存在直至动画完成。但当列表长度变为 0 时,{#if players.length === 0} 会立即切换分支,导致最后一个 <div> 在 fly 动画开始前就被卸载,过渡自然无法执行。
根本原因在于:条件渲染的响应式更新快于过渡生命周期。Svelte 在 players 数组变更后立刻重新计算 {#if} 表达式,并同步销毁旧节点——而此时 out:fly 尚未启动。
✅ 正确做法是分离“视觉过渡”与“逻辑状态”:
- 不再用 players.length === 0 直接驱动 {#if};
- 引入独立的响应式状态(如 noPlayers: boolean);
- 在最后一个元素的 outroend 事件中才更新该状态,确保动画完整播放后再切换 UI。
以下是优化后的完整代码:
<script lang="ts">
import { fly } from 'svelte/transition';
let players = ['Juan', 'John', 'Luis'];
let noPlayers = false; // ✅ 独立控制空状态的标志
</script>
<div class="overflow-hidden">
{#if noPlayers}
<p>No players</p>
{:else}
{#each players as player, playerIndex (player)}
<div
class="flex"
out:fly={{ duration: 500, x: '100vw', opacity: 0 }} {/* 注意:opacity 应为 0,非 100 */}
on:outroend={() => {
// ✅ 动画结束后才更新状态
noPlayers = players.length === 0;
}}
>
<span>{player}</span>
<button
on:click={() => {
players.splice(playerIndex, 1);
players = [...players]; // ✅ 更推荐的响应式赋值写法
}}
>
Remove
</button>
</div>
{/each}
{/if}
</div>? 关键改进说明:
- on:outroend 是 Svelte 提供的标准过渡事件,确保回调在 out 动画完全结束后触发;
- opacity: 0(而非 100)才是符合语义的渐隐值(100 表示不透明,无法实现淡出效果);
- 使用 players = [...players] 替代 players = players,更清晰地表达响应式更新意图(虽后者在多数情况下也生效,但前者是 Svelte 官方推荐模式);
- noPlayers 的更新被严格约束在动画终点,彻底避免竞态问题。
? 进阶提示:若需支持“添加玩家”并恢复列表,只需重置 noPlayers = false 即可,无需额外处理过渡——Svelte 会自动为新插入元素应用 in: 过渡(如需,可配合 in:fly 使用)。
这一模式不仅适用于 fly,也适用于 fade、slide、自定义过渡等所有 out: 场景,是 Svelte 中协调条件渲染与过渡动画的通用最佳实践。

















