首页
/ Svelte 单向转场实战:in: 与 out: 指令的用法、行为差异与底层实现

Svelte 单向转场实战:in: 与 out: 指令的用法、行为差异与底层实现

2026-09-04 11:07:17作者:蔡丛锟

本篇围绕 Svelte 模板语法中的 in:out: 转场指令展开:先讲清它们与双向 transition: 指令在“反转/重播”语义上的本质区别,再结合 Svelte 编译器对指令的处理流程和客户端运行时 $.transition 的实现,解释为什么单向转场在中途被打断时会“从头再来”,帮助你在做列表、面板、弹窗等需要非对称进出动效的场景时做出正确的指令选择。

in:out:transition: 的核心区别

in:out: 指令与 transition: 指令的使用方式完全相同,唯一的区别在于:它们产生的转场不是双向(bidirectional)的。具体表现为两条规则:

  1. 如果元素在 in 转场尚未结束 时所在的块被移除(触发 outro),in 转场不会反转,而是继续播放完毕,与 out 转场同时进行;
  2. 如果一个 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 });
  • 内置转场函数(fadeflyblurdrawflipslide 等)从 svelte/transition 模块导入,参考 svelte/transition 模块说明
  • 参数对象通常包含 delaydurationeasing 等字段,由各内置转场自行定义默认值。

单向语义在交互中意味着什么

transition:in:/out: 的行为差异在实际交互中非常直观:

  • 快速连续切换:使用 transition:fly 时,元素飞出到一半再切回来,会从当前偏移量平滑滑回;使用 in:fly 时,元素重新插入后,fly重新从 y: 200 处完整飞入,而不是接上次未完成的进度。
  • in 过程中立即 outro:in 转场继续播完(例如继续滑到位),同时 out 转场并行启动,两者叠加呈现;这是文档明确描述的行为——“an in transition will continue to 'play' alongside the out transition, 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 | OUTin:fade 只有 INout: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:flyout: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 = truetransitions.js#L275),转场进行中元素不再响应交互;in 开始时恢复为挂载前的原值。测试样例 if-transition-inert 专门验证了 in:fade/out:fade 包裹的元素在嵌套块变化时保持惯性(inert)行为;
  • 事件派发introstart/introend/outrostart/outroend 通过 dispatch_eventCustomEvent 形式派发(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.jsTransitionDirective.js);
  • 需要平滑反转/续播选 transition:;需要“进 A 出 B”或打断即重来的干脆语义选 in: + out:
登录后查看全文
热门项目推荐
相关项目推荐