Svelte 单向转场实战:in: 与 out: 指令的用法、行为差异与底层实现
本篇围绕 Svelte 模板语法中的 in: 与 out: 转场指令展开:先讲清它们与双向 transition: 指令在“反转/重播”语义上的本质区别,再结合 Svelte 编译器对指令的处理流程和客户端运行时 $.transition 的实现,解释为什么单向转场在中途被打断时会“从头再来”,帮助你在做列表、面板、弹窗等需要非对称进出动效的场景时做出正确的指令选择。
in: 和 out: 与 transition: 的核心区别
in: 和 out: 指令与 transition: 指令的使用方式完全相同,唯一的区别在于:它们产生的转场不是双向(bidirectional)的。具体表现为两条规则:
- 如果元素在 in 转场尚未结束 时所在的块被移除(触发 outro),in 转场不会反转,而是继续播放完毕,与 out 转场同时进行;
- 如果一个 out 转场被打断(比如元素又重新插回 DOM,或者同一元素的 outro 被取消/重新触发),转场从头开始(restart from scratch),而不是从当前进度平滑续播。
作为对照,双向的 transition: 指令可以在转场进行中被平滑反转——运行时会让新动画从对方的当前位置(counterpart.t())起步,而不是跳变到新起点。这一点在源码中体现得非常直白(见下文“运行时实现”一节)。
基本用法
下面是文档给出的标准示例:切换 visible 状态时,div 从下方 200px 处飞入(fly),退出时则做淡出(fade)——进与出使用两个不同的转场函数:
<script>
import { fade, fly } from 'svelte/transition';
let visible = $state(false);
</script>
<label>
<input type="checkbox" bind:checked={visible}>
visible
</label>
{#if visible}
<div in:fly={{ y: 200 }} out:fade>flies in, fades out</div>
{/if}
要点说明:
- 参数部分的双花括号
{{ ... }}不是特殊语法,它只是表达式标签里的一个对象字面量(例如in:fly={{ y: 200 }}等价于传入{ y: 200 }); - 内置转场函数(
fade、fly、blur、draw、flip、slide等)从svelte/transition模块导入,参考 svelte/transition 模块说明; - 参数对象通常包含
delay、duration、easing等字段,由各内置转场自行定义默认值。
单向语义在交互中意味着什么
transition: 与 in:/out: 的行为差异在实际交互中非常直观:
- 快速连续切换:使用
transition:fly时,元素飞出到一半再切回来,会从当前偏移量平滑滑回;使用in:fly时,元素重新插入后,fly会重新从y: 200处完整飞入,而不是接上次未完成的进度。 - in 过程中立即 outro:in 转场继续播完(例如继续滑到位),同时 out 转场并行启动,两者叠加呈现;这是文档明确描述的行为——“an
intransition will continue to 'play' alongside theouttransition, rather than reversing”。 - out 被打断:out 转场直接作废并从零重启,没有“反转续播”的优化路径。
这种语义适合进出动效本就不对称的场景(如“弹入 + 淡出”“滑入 + 原位收缩”),也适合希望“打断即重来”的干脆手感。
Local 与 Global 修饰符
in: 和 out: 同样支持 |global 修饰符,语义与 transition: 一致(参见 transition 文档):
{#if x}
{#if y}
<!-- 默认 local:仅在 y 变化时播放 -->
<p in:fade out:fade>only plays when y changes</p>
<!-- global:x 或 y 变化都会触发 -->
<p in:fade|global out:fade|global>plays when x or y change</p>
{/if}
{/if}
默认情况下转场是 local 的:只有元素所在的那个块自身被创建/销毁时才播放,父块变化不会带动它。需要跨块级联时才加 |global。
转场事件与无障碍
带转场的元素会在标准 DOM 事件之外派发四个事件:
| 事件 | 触发时机 |
|---|---|
introstart |
in 转场开始 |
introend |
in 转场完成 |
outrostart |
out 转场开始 |
outroend |
out 转场完成 |
{#if visible}
<p
in:fly={{ y: 200, duration: 2000 }}
out:fade={{ duration: 2000 }}
onintrostart={() => (status = 'intro started')}
onoutrostart={() => (status = 'outro started')}
onintroend={() => (status = 'intro ended')}
onoutroend={() => (status = 'outro ended')}
>
Flies in and out
</p>
{/if}
另外,转场由 Web Animations API 驱动而非 CSS transition,因此全局 @media (prefers-reduced-motion: reduce) 规则把 transition-duration/animation-duration 归零的做法对转场无效。需要为偏好减少动态效果的设备降级动效时,应使用 prefersReducedMotion。
源码解析:指令如何变成单向转场
编译阶段:指令被压缩为标志位
在 TransitionDirective.js 中,编译器把 in: / out: / transition: 三种指令统一转换为一次 $.transition(flags, element, get_fn, get_params) 调用,其中 flags 是一组位标志(定义于 constants.js):
TRANSITION_IN = 1:由in:或transition:置位(node.intro);TRANSITION_OUT = 1 << 1:由out:或transition:置位(node.outro);TRANSITION_GLOBAL:由|global修饰符置位。
也就是说,transition:fade 编译后 flags = IN | OUT,in:fade 只有 IN,out:fade 只有 OUT。方向语义在运行时由这些标志推出。
运行时:direction 的推导
transitions.js 中的 transition(flags, element, get_fn, get_params) 负责创建“转场管理器”并挂到当前 effect 上:
var is_intro = (flags & TRANSITION_IN) !== 0;
var is_outro = (flags & TRANSITION_OUT) !== 0;
var is_both = is_intro && is_outro;
var direction = is_both ? 'both' : is_intro ? 'in' : 'out';
direction('in' | 'out' | 'both')会作为第三个参数 options 传给你的自定义转场函数,因此自定义转场可以根据方向返回不同的时长或曲线——in:fly 和 out:fly 拿到的是同一个函数,但 options.direction 不同。
单向转场为何“打断即重来”
管理器暴露了 in() 与 out(fn) 两个方法(transitions.js#L234-L290):
in() {
element.inert = inert;
if (!is_intro) {
outro?.abort();
outro?.reset?.();
return;
}
if (!is_outro) {
// if we intro then outro then intro again, we want to abort the first intro,
// if it's not a bidirectional transition
intro?.abort();
}
intro = animate(element, get_options(), outro, 1, ...)
}
注意这段注释与逻辑:当元素只声明了 in:(is_outro 为 false)时,再次 intro 会直接 intro?.abort() 旧动画——旧的 in 转场进度被丢弃,新转场从 t=0 重新播放。这正是文档所说“out 被打断则从头开始、in 不反转”的运行时来源。
而双向 transition:(is_outro 为 true)时不会走 abort 分支,而是把上一次的动画作为 counterpart 传给 animate。在 transitions.js#L423-L429:
// for bidirectional transitions, we start from the current position,
// rather than doing a full intro/outro
var t1 = counterpart?.t() ?? 1 - t2;
var delta = t2 - t1;
var duration = options.duration * Math.abs(delta);
即新动画从旧动画当前进度 t1 起步,时长按剩余距离 |delta| 等比缩短——这就是 transition: 能“平滑反转”的机制,也是 in:/out: 刻意不做的事情。
此外还有两个与单向/双向都相关的实现细节:
inert状态:out 转场启动时element.inert = true(transitions.js#L275),转场进行中元素不再响应交互;in 开始时恢复为挂载前的原值。测试样例 if-transition-inert 专门验证了in:fade/out:fade包裹的元素在嵌套块变化时保持惯性(inert)行为;- 事件派发:
introstart/introend/outrostart/outroend通过dispatch_event以CustomEvent形式派发(transitions.js#L17-L21),且在无响应式上下文中派发,避免在事件处理器中读取状态造成副作用。
自定义转场函数中的 direction
自定义转场函数签名为 (node, params, options) => {...},其中 options.direction 对单向指令尤为有用:
/**
* @param {HTMLElement} node
* @param {{ duration?: number }} params
* @param {{ direction: 'in' | 'out' | 'both' }} options
*/
function directional(node, { duration = 400 }, { direction }) {
return {
duration: direction === 'out' ? duration / 2 : duration,
css: (t) => `opacity: ${direction === 'in' ? t : 1 - t}`
};
}
配合 in:directional / out:directional 使用,退出动画可以更短促。返回值中的 css 函数用于生成 Web Animations 关键帧(t 在 in 转场中从 0 走到 1,out 转场中从 1 走到 0,u = 1 - t),能走 css 就不要用 tick——Web 动画可以脱离主线程运行,在低性能设备上更不容易掉帧。返回 tick 的写法(如逐字打印文本)会在转场期间每帧回调,适合 css 无法表达的状态变更。
小结
in:/out:与transition:语法一致、可自由组合(同一个元素可各用一个不同函数),也支持|global、参数对象、转场事件;- 核心差异是单向性:in 转场在 outro 到来时继续播完而非反转,out 转场被中断时从零重启;
- 编译器把方向压缩成
TRANSITION_IN/OUT位标志,运行时据此推导direction并决定“abort 重来”还是“按counterpart.t()续播”(见 transitions.js 与 TransitionDirective.js); - 需要平滑反转/续播选
transition:;需要“进 A 出 B”或打断即重来的干脆语义选in:+out:。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00