拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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 完全不同。文档给出的核心规则有三条动画只在 keyed each block 的内容被重新排序re-ordered时触发元素被添加或删除时不会运行动画只有当某个已存在的数据项在 each block 中的索引发生变化时才会触发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 中的某个直接子节点是RegularElement或SvelteElement并带有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.tsexport 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的实现细节值得展开看。它并不只是简单地做translatevar { 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/fy、tx/ty再换算出初始位移dx/dy这样即使元素设置了非中心的变换原点动画也不会错位考虑父级 CSS zoomget_zoom()会沿着父元素链累乘zoom计算值或读取currentCSSZoom保证在缩放容器内位移换算正确处理尺寸变化除位移外还计算相对缩放dsx/dsyfrom.width / to.width最终的css回调同时输出translate和scalecss: (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、一个包含from与to的几何状态对象、以及你在模板里传入的任意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()接受的关键帧对象并按规范做驼峰化cssFloat、cssOffset--自定义属性保持原名。文档给出的完整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函数它在动画播放过程中被逐帧调用参数同样是t和u。文档中的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 状态。运行时如何“冻结”元素完成 FLIPmeasure / apply / fix / unfix理解animate:为什么能跑通还要看运行时管理器的fix/unfix阶段transitions.jsmeasure重排前用getBoundingClientRect()记录fromfix把元素临时改为position: absolute并固定width/height保存原值到original_styles以便还原如果这导致位置偏移再叠加一个translate(...)把元素“钉”在视觉原点上从而让列表其它元素先行移动而不带动它apply重排后记录to调用你的动画函数并创建 Web Animationunfix动画结束后恢复position、width、height、transform原值。fix中还有一个防御性细节如果element.getAnimations().length非零元素正被其它动画例如 crossfade 占着 transform则跳过定位注释解释了原因是“正在运行的动画施加的样式优先级更高会导致元素跳到左上角”。另外针对svelte:element标签动态变化的场景animation()会复用已存在的管理器、只替换nodes.a.element而不是新建一个。缓动函数方面flip默认使用的cubicOut以及linear、sineIn/Out/InOut、quad*、cubic*、expo*、circular、elastic*、back*、bounce*等全套缓动函数都来自 easing/index.js通过svelte/easing子模块导出可在自定义动画的easing字段中任意选用。小结使用 animate: 的要点清单animate:只配合keyed{#each}使用且必须写在 each 的直接子元素上只在重排时触发增删元素不触发——若需要进出场动画请改用 transitions参数用对象字面量animate:flip{{ delay: 500 }}传递flip支持delay、duration可为(len) number、easing三个参数自定义函数接收(node, { from, to }, params)返回{ delay, duration, easing, css, tick }from/to是重排前后的DOMRect优先用cssWeb 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),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门