news 2026/9/5 18:53:21

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Svelte animate: 指令详解:keyed each 列表重排动画的 FLIP 原理与自定义动画函数

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

【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte

本文基于 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标志。注释里明确写道:“Sinceanimate: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 实现 中的默认值解构:

参数类型默认值说明
delaynumber0动画开始前的延迟(毫秒)
durationnumber \| (len) => number(d) => Math.sqrt(d) * 120可以是固定毫秒数,也可以是一个接收元素位移距离d的函数,按距离平方根动态计算时长
easing(t) => numbercubicOut缓动函数,通常从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 afterbind: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.md、svelte/animate参考页 21-svelte-animate.md、内置实现 animate/index.js、类型定义 animate/public.d.ts、编译器 AnimateDirective.js 与 EachBlock.js、运行时 transitions.js。

【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 18:51:18

电赛接线告别蜘蛛网:从短路到可靠的硬件设计

电赛调试阶段有个非常典型的画面&#xff1a;杜邦线跨成一团&#xff0c;传感器信号线贴着电机功率线走&#xff0c;芯片引脚上直接飞出几根细漆包线&#xff0c;焊点附近连热缩管都没套。第一次上电可能还能跑&#xff0c;一旦把板子装进车模、机械臂或者无人机里&#xff0c;…

作者头像 李华
网站建设 2026/9/5 18:47:43

如何在家复刻老乡鸡菜品:CookLikeHOC 菜品复刻完整指南

如何在家复刻老乡鸡菜品&#xff1a;CookLikeHOC 菜品复刻完整指南 【免费下载链接】CookLikeHOC &#x1f962;像老乡鸡&#x1f414;那样做饭。已添加2026年发布的《老乡鸡菜品溯源报告 2.0中新出现的菜品。主要部分于2024年完工&#xff0c;非老乡鸡官方仓库。文字来自《老乡…

作者头像 李华
网站建设 2026/9/5 18:46:07

SouljaBoy掌机登录翻车:从单点故障到首发产品可用性复盘

SouljaBoy 新掌机最近热度不低&#xff0c;但出圈的原因不是配置多强、游戏多好&#xff0c;而是翻车&#xff1a;官方定价 299 美元&#xff0c;属于典型的“中档价位产品”&#xff0c;结果买家到手后连账号登录这关都过不去&#xff0c;进不了系统&#xff0c;更谈不上跑游戏…

作者头像 李华
网站建设 2026/9/5 18:43:52

用ESP32-C3自制低频示波器:从ADC采样到波形显示的完整实践

很多嵌入式开发者在入门时都会被同一个问题拦住&#xff1a;想调一个 I2C 通信、看 PWM 占空比或者确认电源纹波是否异常&#xff0c;手边却没有示波器。买一台入门级台式示波器通常要大几千&#xff0c;于是 “用 ESP32-C3 自己做一个示波器” 就成了一个看起来很诱人的方案。…

作者头像 李华
网站建设 2026/9/5 18:43:26

全站自动化脚本开发实战:从Python与Shell技术栈到TUS/UO项目架构

简介&#xff1a;本资源是一套完整的《Ultima Online》&#xff08;UO&#xff09;游戏服务器原始全站脚本体系&#xff0c;面向UO私服开发者、服务器管理员及MUD类游戏脚本进阶学习者&#xff0c;用于快速部署、定制与维护TUS&#xff08;The Ultimate Server&#xff09;架构…

作者头像 李华