首页
/ Svelte animate: 指令详解:keyed each 列表重排动画的 FLIP 原理与自定义动画函数

Svelte animate: 指令详解:keyed each 列表重排动画的 FLIP 原理与自定义动画函数

2026-09-04 10:08:11作者:舒璇辛Bertina

本文基于 Svelte 官方模板语法文档 documentation/docs/03-template-syntax/16-animate.md 展开,系统讲解 animate: 指令的触发条件、内置 flip 动画的参数与默认值、自定义动画函数(custom animation functions)的完整签名与 css/tick 回调语义,并结合 Svelte 编译器与运行时源码,剖析指令从模板编译到 Web Animations API 播放的完整调用链,帮助读者写出既正确又高性能的列表重排动画。

动画的触发条件:只在 keyed each 重排时运行

animate: 指令的行为边界与普通 transition 完全不同。文档给出的核心规则有三条:

  1. 动画只在 keyed each block 的内容被重新排序(re-ordered)时触发
  2. 元素被添加或删除时不会运行动画,只有当某个已存在的数据项在 each block 中的索引发生变化时才会触发;
  3. animate: 指令必须写在 keyed each block 的直接子元素(immediate child)上

最小可用示例:

<!-- When `list` is reordered the animation will run -->
{#each list as item, index (item)}
	<li animate:flip>{item}</li>
{/each}

注意 (item) 是 key 表达式——没有 key 的 each block 是“非 keyed”的,animate: 对其无效。

从源码结构可以印证这三条规则。在 EachBlock 客户端转换 中,编译器在编译期就能确定一个 each block 是否带动画:只有当 node.key 存在、且 body 中的某个直接子节点是 RegularElementSvelteElement 并带有 AnimateDirective 属性时,才会打上 EACH_IS_ANIMATED 标志。注释里明确写道:“Since animate: can only appear on elements that are the sole child of a keyed each block, we can determine at compile time whether the each block is animated or not (in which case it should measure animated elements before and after reconciliation)”。也就是说,编译器会把“重排前测量 / 重排后测量”织入每个带动画的 keyed each block 的调和(reconciliation)流程中。

分析阶段 还有一个容易忽略的约束:animate: 的参数表达式不允许包含 await,否则会触发 illegal_await_expression 编译错误。

内置动画函数 flip 及其参数

Svelte 的动画可以用内置动画函数或自定义函数。svelte/animate 模块当前只提供 flip 这一个内置函数,实现位于 animate/index.js

flip 这个名字来自经典的 [First, Last, Invert, Play] 动画技术:先记录元素“第一(First)”位置,再让布局到达“最后(Last)”位置,随后用“反向(Invert)”的 transform 把元素视觉上拉回起点,最后“播放(Play)”动画。

它的参数类型定义在 animate/public.d.ts

export interface FlipParams {
	delay?: number;
	duration?: number | ((len: number) => number);
	easing?: (t: number) => number;
}

结合 flip 实现 中的默认值解构:

参数 类型 默认值 说明
delay number 0 动画开始前的延迟(毫秒)
duration number | (len) => number (d) => Math.sqrt(d) * 120 可以是固定毫秒数,也可以是一个接收元素位移距离 d 的函数,按距离平方根动态计算时长
easing (t) => number cubicOut 缓动函数,通常从 svelte/easing 导入

flip 的实现细节值得展开看。它并不只是简单地做 translate

var { delay = 0, duration = (d) => Math.sqrt(d) * 120, easing = cubicOut } = params;

var style = getComputedStyle(node);
// find the transform origin, expressed as a pair of values between 0 and 1
var [ox, oy] = style.transformOrigin.split(' ').map(parseFloat);
ox /= node.clientWidth;
oy /= node.clientHeight;

// calculate effect of parent transforms and zoom
var zoom = get_zoom(node);
var sx = node.clientWidth / to.width / zoom;
var sy = node.clientHeight / to.height / zoom;

可以看到它做了三件容易被忽视的事:

  • 考虑 transform-origin:分别求出起点与终点处 transform origin 的绝对坐标 fx/fytx/ty,再换算出初始位移 dx/dy,这样即使元素设置了非中心的变换原点,动画也不会错位;
  • 考虑父级 CSS zoomget_zoom() 会沿着父元素链累乘 zoom 计算值(或读取 currentCSSZoom),保证在缩放容器内位移换算正确;
  • 处理尺寸变化:除位移外还计算相对缩放 dsx/dsyfrom.width / to.width),最终的 css 回调同时输出 translatescale
css: (t, u) => {
	var x = u * dx;
	var y = u * dy;
	var sx = t + u * dsx;
	var sy = t + u * dsy;
	return `transform: ${transform} translate(${x}px, ${y}px) scale(${sx}, ${sy});`;
}

如果元素原本带有 transform(非 none),它会保留在输出字符串的开头,避免动画期间覆盖已有变换。

动画参数:{{...}} 是对象字面量而非特殊语法

与 actions 和 transitions 一样,动画可以携带参数。文档特别提醒:双花括号 {{curlies}} 并不是 Svelte 的特殊语法,它只是表达式标签内部的一个对象字面量

{#each list as item, index (item)}
	<li animate:flip={{ delay: 500 }}>{item}</li>
{/each}

编译层面,客户端 AnimateDirective 转换 会把指令展开为一个 $.animation 调用:

$.animation(node, () => flip, () => ({ delay: 500 }));

两个细节:

  • 指令名和参数表达式都被包成 thunk(() => ...),延迟到运行时求值;
  • 该语句被推入 after_update,源码注释解释了原因:“in after_update to ensure it always happens after bind:this”,即保证 bind:this 先拿到元素引用,动画管理器再注册到该元素上;
  • 如果参数表达式是异步的(依赖 store 等),还会额外包一层 $.run_after_blockers 等待阻塞项完成。

自定义动画函数

函数签名

自定义动画函数接收三个参数:目标元素 node、一个包含 fromto 的几何状态对象、以及你在模板里传入的任意 params。文档给出的签名(结合 AnimationConfig 的 TypeScript 定义):

/**
 * @param {HTMLElement} node
 * @param {{ from: DOMRect; to: DOMRect }} states
 * @param {any} params
 */
function whizz(node, { from, to }, params) { /* ... */ }

其中 animation 对象的两个关键属性:

  • from:元素在起始位置DOMRect(重排前测量);
  • to:列表重排并更新 DOM 之后,元素最终位置DOMRect

运行时如何填充这两个值,可以在 transitions.js 中的 animation() 函数 中看到:每个带 animate: 的元素会在其 effect 的 nodes.a 上挂一个“动画管理器”,measure() 在重排前记录 from = element.getBoundingClientRect()apply() 在重排后记录 to,并只有当左右上下四条边任一发生变化时才真正调用你的动画函数:

if (
	from.left !== to.left ||
	from.right !== to.right ||
	from.top !== to.top ||
	from.bottom !== to.bottom
) {
	const options = get_fn()(this.element, { from, to }, get_params?.());
	animation = animate(this.element, options, undefined, 1, () => {}, () => { ... });
}

返回对象与 css 回调

动画函数应返回一个配置对象,字段与 AnimationConfig 一致:

export interface AnimationConfig {
	delay?: number;
	duration?: number;
	easing?: (t: number) => number;
	css?: (t: number, u: number) => string;
	tick?: (t: number, u: number) => void;
}

如果返回对象带有 css 方法,Svelte 会为元素创建一个Web Animation(即 Element.animate())来播放动画。css 回调的语义:

  • t 是从 0 走到 1 的值,已经应用过 easing 函数
  • u 恒等于 1 - t
  • 该函数会在动画开始前被反复调用,用不同的 t/u 取样,生成关键帧。运行时通过 css_to_keyframe 把返回的 CSS 字符串(如 transform: translate(12px, 0px);)解析为 Element.animate() 接受的关键帧对象,并按规范做驼峰化(cssFloatcssOffset-- 自定义属性保持原名)。

文档给出的完整 css 示例:

<!--- file: App.svelte --->
<script>
	import { cubicOut } from 'svelte/easing';

	/**
	 * @param {HTMLElement} node
	 * @param {{ from: DOMRect; to: DOMRect }} states
	 * @param {any} params
	 */
	function whizz(node, { from, to }, params) {
		const dx = from.left - to.left;
		const dy = from.top - to.top;

		const d = Math.sqrt(dx * dx + dy * dy);

		return {
			delay: 0,
			duration: Math.sqrt(d) * 120,
			easing: cubicOut,
			css: (t, u) => `transform: translate(${u * dx}px, ${u * dy}px) rotate(${t * 360}deg);`
		};
	}
</script>

{#each list as item, index (item)}
	<div animate:whizz>{item}</div>
{/each}

这个例子里的 duration: Math.sqrt(d) * 120 正是内置 flip 的默认公式,rotate(${t * 360}deg) 则展示了用 t 驱动旋转角度的典型写法。

tick 回调与性能取舍

自定义动画函数也可以返回 tick 函数,它在动画播放过程中被逐帧调用,参数同样是 tu。文档中的 tick 示例:

<!--- file: App.svelte --->
<script>
	import { cubicOut } from 'svelte/easing';

	/**
	 * @param {HTMLElement} node
	 * @param {{ from: DOMRect; to: DOMRect }} states
	 * @param {any} params
	 */
	function whizz(node, { from, to }, params) {
		const dx = from.left - to.left;
		const dy = from.top - to.top;

		const d = Math.sqrt(dx * dx + dy * dy);

		return {
			delay: 0,
			duration: Math.sqrt(d) * 120,
			easing: cubicOut,
			tick: (t, u) => Object.assign(node.style, { color: t > 0.5 ? 'Pink' : 'Blue' })
		};
	}
</script>

{#each list as item, index (item)}
	<div animate:whizz>{item}</div>
{/each}

文档在这里附有一条重要性能提示:只要可能用 css 就不要用 tick——Web Animation 可以脱离主线程运行,避免在低性能设备上产生卡顿。tick 每一帧都要在主线程执行你的 JS 并直接改样式,仅用于 css 无法表达的场景(比如切换非 CSS 状态)。

运行时如何“冻结”元素完成 FLIP:measure / apply / fix / unfix

理解 animate: 为什么能跑通,还要看运行时管理器的 fix/unfix 阶段(transitions.js):

  • measure:重排前用 getBoundingClientRect() 记录 from
  • fix:把元素临时改为 position: absolute 并固定 width/height(保存原值到 original_styles 以便还原),如果这导致位置偏移,再叠加一个 translate(...) 把元素“钉”在视觉原点上,从而让列表其它元素先行移动而不带动它;
  • apply:重排后记录 to,调用你的动画函数并创建 Web Animation;
  • unfix:动画结束后恢复 positionwidthheighttransform 原值。

fix 中还有一个防御性细节:如果 element.getAnimations().length 非零(元素正被其它动画,例如 crossfade 占着 transform),则跳过定位,注释解释了原因是“正在运行的动画施加的样式优先级更高,会导致元素跳到左上角”。另外,针对 <svelte:element> 标签动态变化的场景,animation() 会复用已存在的管理器、只替换 nodes.a.element,而不是新建一个。

缓动函数方面,flip 默认使用的 cubicOut 以及 linearsineIn/Out/InOutquad*cubic*expo*circularelastic*back*bounce* 等全套缓动函数都来自 easing/index.js,通过 svelte/easing 子模块导出,可在自定义动画的 easing 字段中任意选用。

小结:使用 animate: 的要点清单

  • animate: 只配合 keyed {#each} 使用,且必须写在 each 的直接子元素上;
  • 只在重排时触发,增删元素不触发——若需要进出场动画请改用 transitions;
  • 参数用对象字面量 animate:flip={{ delay: 500 }} 传递,flip 支持 delayduration(可为 (len) => number)、easing 三个参数;
  • 自定义函数接收 (node, { from, to }, params),返回 { delay, duration, easing, css, tick }from/to 是重排前后的 DOMRect
  • 优先用 css(Web Animation,可离主线程),必要时才用 tick
  • 参数表达式不能包含 await,否则编译报错。

参考仓库文件:模板语法文档 16-animate.md、keyed each 文档 03-each.mdsvelte/animate 参考页 21-svelte-animate.md、内置实现 animate/index.js、类型定义 animate/public.d.ts、编译器 AnimateDirective.jsEachBlock.js、运行时 transitions.js

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341