列表里删掉一条数据,界面“啪”地一下塌下去,上面几行瞬间瞬移,用户眨个眼还以为自己点错了;表单里加了几个自定义字段,整块布局硬生生向上弹了一截;抽屉展开的时候内容直接闪现,没有任何过渡。这类“跳变”我改过太多次,早年是用 CSS
max-height
硬撑,后来是手写 FLIP,再后来干脆把整个动画框架搬进项目里,为了一个淡入淡出背了一堆依赖。
自动过渡动画
这条路线之所以值得单独拿出来讲,是因为它把“结构变化时的过渡”这件苦力活,压缩到了几乎零成本的调用上。
AutoAnimate
就是这条路线里最轻的一个:给父容器挂上一次,子节点的增删、移动、排序自动带上过渡,不用写 keyframes,不用算坐标,也不用给每个列表项单独挂
transition
。它解决的是“DOM 结构发生变化时,元素位置和显隐的平滑衔接”这个具体问题,而不是包办整套动效体系。适合的人群也很明确:做后台管理、表单动态字段、任务清单、折叠面板、标签筛选结果这类“结构会变但动画不需要炫技”的开发者;以及那些不愿意为了一次淡入就把整套动画运行时搬进项目的团队。下面我按自己实际落地时的思路,把这个库拆到能直接抄作业的程度。
1. 先想清楚它到底在解决什么问题
1.1 传统写法为什么会让人越写越烦
手写 CSS
transition
的舒适区很窄:它擅长处理“同一个元素的属性值变化”,比如宽度从 200px 变 300px、颜色从灰变蓝。但界面里真正让人别扭的,往往不是属性变化,而是
节点级的变化
——插入新行、删除旧行、把第三项拖到第一位。这些操作在 DOM 层面是“旧的没了、新的来了”,元素本身并没有从 A 属性过渡到 B 属性,CSS 根本无从下手,浏览器只会老老实实地重新排版,然后一次性把新结果画出来。
于是就有了 FLIP 这套手工方案:First 记录变化前每个元素的位置,Last 记录变化后的位置,Invert 用
transform
把元素“拉回”旧位置,Play 让它动画回到真实位置。思路很优雅,但落到代码里就是一堆琐碎活:要在 DOM 更新前
getBoundingClientRect()
快照、要在更新后立刻再读一次、要给每个元素算
translate
差值、要处理新元素的进入和旧元素的离开、还要在动画结束后把临时加的样式清干净。一个列表做下来两三百行,稍微改改布局又得重新调。
更麻烦的是“删除”这件事。元素一旦从 DOM 里摘掉,浏览器就不会再为它绘制任何东西,你根本没法给它做离场动画。想让被删的行淡出,就得先把节点留住、播完动画再摘,这套延迟移除的逻辑手写起来很容易出 bug,尤其是和框架的渲染队列混在一起的时候。
1.2 AutoAnimate 的定位:只做“结构过渡”,不碰复杂编排
AutoAnimate 的边界划得很清楚:它监听一个父容器的 直接子节点 增删变化,为受影响的位置变化和显隐做过渡。就这一件事,别的都不管。它不做时间轴编排,不做路径动画,不做滚动驱动的视差,也不替你管理复杂的状态机。你想做那种一个按钮点下去,五个元素依次飞出、旋转、落位的效果,它不是干这个的。
这个定位带来的好处是体积和行为都可控。它没有自己的渲染循环,把动画交给了浏览器原生的 Web Animations API,自己只在 DOM 变化的那一刻做一次计算。代价是它只能处理“位置 + 缩放 + 透明度”这一组变化,做不了太花哨的东西。但对 90% 的界面来说,位置和显隐恰恰就是全部——用户感知到的“流畅不流畅”,绝大部分来自元素有没有平滑地挪到新位置,而不是有没有弹性回弹。
所以我在选型时会先问一句:这个交互需要的是“让变化看起来连续”,还是“让变化看起来惊艳”?前者交给它,后者老老实实上专业动画库。把这两类需求混在一起,往往是项目后期最难维护的部分。
1.3 什么场景适合,什么场景要绕开
适合的场景很集中:动态增删的列表(待办、标签、消息流)、筛选和排序后的结果重排、折叠面板与手风琴、动态表单字段、卡片网格的增删、树形菜单展开收起。这些场景的共同特点是——动画是“辅助理解”,不是“表达个性”。用户需要的是“我知道它从哪儿来、到哪儿去了”,而不是“哇”。
要绕开的场景也得说清楚。第一类是
虚拟滚动列表
,因为可视区外的节点会被频繁复用和替换,DOM 增删量极大且没有实际语义,动画会变成闪烁;第二类是
单元素的内容切换
,比如同一个容器里
v-if
切换两个完全不同的子组件,如果两个组件尺寸差异很大,直接套用会出现高度突变;第三类是
高频更新的实时数据
,比如每秒刷新的行情列表,动画反而会拖慢感知速度。这三类我是明确不用它的,用了就是给自己找麻烦。
注意:它监听的是直接子节点(
childList),不会递归到孙子层。嵌套列表想每层都有动画,必须每层各挂一次,这一点新手最容易漏。
2. 拆开看它的实现:三个零件拼起来的东西
2.1 变化监听:MutationObserver 是入口
整个库的入口是一个
MutationObserver
,观察目标就是传进去的父元素,配置里只开
childList
,不开
subtree
、不开
attributes
。这个选择很关键:开
subtree
意味着孙子节点的任何风吹草动都会触发回调,在复杂页面里回调频率会高得离谱,而且大部分变化并不需要动画。只盯一层,是性能和行为可预测性的双重取舍。
回调触发时,它做的第一件事不是立刻动画,而是先把当前所有子元素的位置和尺寸批量读一遍,跟上一轮存的快照比对。这里有个容易忽略的细节:
所有读取集中在一起做,所有写入也集中在一起做
。浏览器有个“强制同步布局”的坑——如果你读一次
getBoundingClientRect()
、写一次样式、再读一次,每次读都会逼着浏览器立刻把前面所有待处理样式算完,一帧里来上几十次,帧率直接崩。先全读、再全写,是绕开这个坑的标准做法,它在这个环节做得是对的。
顺带一提,DOM 的 mutation 回调本身是微任务批次触发的,也就是说同一轮里连删三个节点、再加两个,只会触发一次回调,这对性能是好事。但这也意味着你的快照必须足够“整”,不能指望一次只处理一个节点。
2.2 位置差计算:为什么用文档坐标
比对的核心数据是每个元素的矩形框——左上角坐标加宽高。变化前后各拿一份,差值就是需要补偿的距离。对老元素来说,如果它原来在
y=100
,现在跑到了
y=240
,那它就该从
translate(0, -140px)
动画到
translate(0, 0)
,视觉上就是“滑下去”。这就是 FLIP 里的 Invert + Play。
用矩形框而不是直接读
offsetTop
,是因为矩形框拿到的是
最终渲染结果
,它把父级的 flex 排序、grid 布局、margin 折叠、绝对定位全都算进去了。你不需要理解元素是怎么被排到那个位置的,只需要知道它在屏幕上实际在哪。这个“以结果为准”的思路,是整个库能适配任意布局的根本原因——不管你的容器是 flex、grid 还是 float 老古董,它都一视同仁。
代价是它必须依赖渲染后的真实坐标。如果父级链路上有
transform
、
filter
、
zoom
这类会建立新包含块的属性,
getBoundingClientRect()
返回的是
变换后
的坐标,而库又用
transform
去做动画,两者会叠在一起,出现位置偏移。这个坑我后面单独说。
2.3 为什么用 WAAPI 而不是 CSS transition
很多人第一反应是:既然算出了差值,为什么不加个
transition: transform 250ms
然后改
transform
值?答案是可控性。CSS transition 一旦设上,后续任何对
transform
的修改都会触发过渡,包括你不想要的那些;而且想“只在这一次生效”,就得加类、等
transitionend
、再删类,中间还得防着事件不触发。
Web Animations API 提供的是命令式的单次动画:
element.animate(keyframes, options)
,返回一个动画对象,有
finished
promise、有
cancel()
、有
playbackRate
。播完自动结束,不会残留状态,不会影响元素后续的样式计算。对于“每次变化都临时算一组关键帧”的场景,这个模型天然贴合。而且 WAAPI 的动画跑在合成线程上(对
transform
和
opacity
而言),主线程就算被 JS 占住,动画也不会卡住。
2.4 进场和离场为什么长得不一样
新增元素的处理不是简单的位置补偿,因为它没有“旧位置”可参照。库的做法是给它一个从“略小 + 透明”到“原尺寸 + 不透明”的组合动画,具体是从
scale
小于 1、
opacity
为 0 起步,播到
scale(1)
和
opacity: 1
。这个组合在视觉上像是“元素从原位置长出来”,比单纯淡入更有存在感,又不需要知道它从哪个方向来。
删除元素则要绕一道弯:节点被移除后浏览器不再绘制它,动画无处附着。所以库的做法是先把即将消失的节点“留住”,让它照常播完离场动画,播完再真正从 DOM 里摘掉。这也是为什么你偶尔会看到被删掉的那一行还在原位淡出一小会儿——它不是 bug,是设计。这段延迟里节点仍然占着文档流的位置,所以容器高度不会在动画开始的一瞬间塌陷,整体观感更连续。
理解了这个不对称,很多“奇怪现象”就顺了:比如删掉最后一个元素时容器高度是渐变的,而删掉中间元素时其他元素是滑上来的,两者节奏本来就不同。
3. 上手实操:接入方式与参数怎么定
3.1 最小可用版本
不管用什么框架,核心调用都是“拿到父元素,传进去”。原生环境里就是这么两行:
import autoAnimate from '@formkit/auto-animate'
const list = document.querySelector('#task-list')
const controller = autoAnimate(list)
// 之后你随便增删 list 的子节点,动画自动生效
list.appendChild(createItem('新任务'))
list.removeChild(list.firstElementChild)
返回值是个控制器,带
enable()
、
disable()
、
isEnabled()
三个方法。
disable()
不是“暂停当前动画”,而是直接断开观察、清掉快照。我在做“用户手动关闭动效”这个设置项时用的就是它,比到处传布尔值判断干净。
框架侧各有封装形态,但内核一致:
// React
import { useAutoAnimate } from '@formkit/auto-animate/react'
function TaskList({ tasks }) {
const [parent] = useAutoAnimate({ duration: 220 })
return (
<ul ref={parent}>
{tasks.map(t => <li key={t.id}>{t.title}</li>)}
</ul>
)
}
<!-- Vue:先注册指令,模板里直接用 -->
<script setup>
import { vAutoAnimate } from '@formkit/auto-animate/vue'
</script>
<template>
<ul v-auto-animate="{ duration: 220 }">
<li v-for="t in tasks" :key="t.id">{{ t.title }}</li>
</ul>
</template>
Svelte 那边它本身就是一个 action,
use:autoAnimate
直接挂。三个框架的共同点是:
必须挂在产生增删的那个父元素上,且子元素必须有稳定的 key
。第二条尤其重要,React 里用数组下标当 key,删中间一项会导致后面所有项的 key 集体前移,框架会认为是“内容变了但节点没变”,DOM 上根本没发生增删,动画自然也就不会触发,最后表现为“删了但没动,只是文字变了”。这个问题我在两个项目里都遇到过,排查半天才发现是 key 的锅。
3.2 配置项逐个拆
可配的东西不多,但每一项都有实际影响:
| 配置项 | 默认值 | 作用 | 我的实际取值建议 |
|---|---|---|---|
duration
| 250 | 单次动画时长,毫秒 | 列表 180~220;面板 250~300;高频操作压到 150 |
easing
|
ease-in-out
| 缓动曲线 |
默认够用;元素移动距离大时换
ease-out
收尾更利落
|
delay
| 0 | 延迟开始 | 基本不用;需要错峰时优先用插件按索引算 |
disrespectUserMotionPreference
|
false
| 是否无视系统“减少动效”偏好 |
保持
false
,除非客户明确要求强制播放
|
关于最后一项,值得展开说。系统层面的“减少动态效果”开关,对晕动症用户是刚需,不是可选项。这个库默认尊重该设置,检测到用户开了这个开关就直接跳过动画、瞬间完成变化。
disrespectUserMotionPreference: true
会强行无视它。我见过有团队为了让演示效果一致把它打开,这是典型的拿可访问性换观感,不建议。
duration
的取值逻辑也不是拍脑袋。人对“操作有反馈”的感知阈值大概在 100ms 上下,低于这个数会觉得“没动”,高于 400ms 会觉得“卡”。列表项之间的位移距离通常在几十像素量级,200ms 左右刚好能看清轨迹又不拖沓。但如果是大面积卡片重排,位移可能到 300px 以上,同样的 200ms 会显得很急,这时候拉到 300ms 反而更自然。所以我的做法是按容器类型分档,而不是全局一个值。
// 按场景分档,写在统一的配置文件里,避免到处魔法数字
export const MOTION = {
listItem: { duration: 200, easing: 'ease-out' },
panel: { duration: 280, easing: 'ease-in-out' },
denseGrid: { duration: 160, easing: 'ease-out' },
}
3.3 高度变化这类“非节点”变化它管不了
这一点必须提前说清楚,否则一定会在实现折叠面板时踩坑。
childList
监听的是子节点的增删,如果只是某个子元素的
height
从 0 变成
auto
、或者用
v-show
切换
display
,DOM 结构压根没变,它不会有任何反应。想让面板展开有过渡,要么切换时真的增删节点(
v-if
),要么把高度过渡单独用 CSS 处理,两条路各走各的。
我一般的选择是:
面板内容用
v-if
+ 挂 AutoAnimate,高度交给 CSS
。这样展开时高度是 CSS 平滑撑开,内容元素是淡入,两层叠加的观感比较完整。如果只做其中一层,总有一半是硬的。
4. 几个真实落地的场景拆解
4.1 动态列表:增删和筛选结果
这是最典型的用法,也是收益最直接的地方。待办清单勾选完成后从“进行中”移到“已完成”,本质上是两个列表各删一个、各加一个。如果两个列表各自挂了 AutoAnimate,视觉上就是一条从上面滑下去、另一端淡入,用户能明确感知到“它转移了”。
筛选场景稍微复杂一点。输入关键词,列表从 30 条缩到 3 条,剩下 3 条的
key
没变但位置变了,库会算出它们的新位置并做位移动画;被过滤掉的 27 条走离场动画。这个过程里最容易出问题的是容器高度:如果容器有
min-height
,收缩过程会很突兀。我的处理是给容器一个和最终结果无关的固定最小高度,让收缩发生在容器内部,不去冲击页面整体布局。
function FilterList({ all, keyword }) {
const [parent] = useAutoAnimate(MOTION.listItem)
const visible = all.filter(item => item.title.includes(keyword))
return (
<ul ref={parent} style={{ minHeight: 320 }}>
{visible.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
)
}
4.2 拖拽排序:动画和拖拽库的配合
排序列表是它的高光场景,但也是坑最多的。拖拽库(比如基于 HTML5 拖放或者指针事件实现的那类)在拖动过程中会频繁地移动 DOM 节点来“预览”插入位置,如果 AutoAnimate 同时对每次移动都做动画,就会出现“你拖着一个元素,其他元素在疯狂抖动”的效果。
我的处理是分阶段开关:拖拽开始时
controller.disable()
,松手落位、DOM 稳定之后再
controller.enable()
,然后手动触发一次重排(比如原地删一个再插一个,或者干脆借助库的控制器重新绑定)。这样拖动过程是即时的,落位那一下才有过渡。
const controller = autoAnimate(listEl)
dragStart(() => controller.disable())
dragEnd(() => {
controller.enable()
// 落位后重新测量,下一次变化才有正确基准
requestAnimationFrame(() => { /* 触发一次重排 */ })
})
另一个注意点是拖拽时的占位元素。如果拖拽库插入了一个透明的占位
div
作为落点提示,那个
div
也是容器的子节点,它的增删同样会触发动画,导致整列元素上下窜动。解决办法是给占位元素加一个固定的类名,在插件里过滤掉,或者干脆让占位元素用绝对定位脱离文档流。
4.3 卡片网格与动态表单
网格布局的重排是它比较能打的场景。卡片从两列变三列、删掉一张后后面的补齐位置,这些在 grid 里都是位置变化,库处理起来和列表没有区别。要注意的是 grid 的
gap
和
align-items
会影响矩形框的读取结果——如果卡片高度不一致(比如
align-items: start
),删掉一张高卡片会导致后面的卡片整体上移,位移距离可能是几百像素,这时候 200ms 会显得很急,建议按最大可能位移适当加时长。
动态表单字段是我用得最多的地方。用户点“添加一项”,新字段从下方长出来,原有字段顺势下移,视觉上非常自然。这里有个细节:新字段插入时,如果它的初始内容是空的、高度靠 CSS 撑开,可能会出现“动画播到一半高度才定下来”的抖一下。稳妥做法是让新字段在插入时就有一个确定的最小高度,别让它依赖异步加载的内容。
4.4 路由与局部视图切换要谨慎
用它在同一个容器里切换两个页面的内容,是很多人的第一反应。但这里有个硬伤:两个页面的内容高度往往差异很大,切换瞬间容器高度会突变,即使子元素有淡入淡出,整体观感仍然会“弹”。而且路由切换时旧页面卸载、新页面挂载是在同一批 DOM 变更里发生的,两者的动画会撞在一起。
我的建议是: 路由级切换不要用它 ,用专门的过渡组件或者干脆不做;局部视图切换(比如标签页内容)可以用,但前提是各标签内容高度接近,或者给容器设一个能容纳最大内容的固定高度。为了动画而让布局失去自适应能力,这笔账不划算。
5. 踩过的坑和排查清单
5.1 位置偏移:滚动容器与变换祖先
这是出现频率最高的一个问题。表现是:动画方向不对,元素从错误的位置飞过来,或者动画结束后“跳”一下归位。根源多半在坐标系。矩形框拿到的是相对视口的坐标,而库在计算差值时需要的是同一套坐标系下的前后两个值。如果父容器是滚动容器,页面滚动会让两次读取之间产生额外的纵向偏移,这个偏移会被误当成“元素移动了”,于是动画就歪了。
抓这个问题的方法很简单:在浏览器的动画面板里看关键帧的
translate
值,和你预期的手算值对比,对不上就说明快照受到了干扰。解决思路有三个:把 AutoAnimate 挂到内容层(滚动容器内部那个真正增删子节点的元素)而不是滚动容器本身;在滚动事件里
disable()
、停下后
enable()
;或者干脆给滚动容器一个固定的内部包装层,让库只看到包装层的子节点变化。
变换祖先的问题更隐蔽。如果从容器向上数,任何一层有
transform
、
translate
、
scale
、
rotate
、
filter
或者
will-change: transform
,这层就会成为新的包含块,矩形框返回的坐标已经是变换后的结果,而库又叠了一层自己的
translate
,两个变换相乘,位置就飘了。我遇到过一次是外层有个“入场缩放”的容器,
transform: scale(0.98)
一直挂着没清,导致里层列表的动画全部偏了 2%。排查手法是临时把祖先链上的
transform
全清掉,看是否恢复,恢复了就是这个问题。
5.2 key 不稳定导致的“鬼畜”
这个坑我在三处踩过。第一种是用数组下标当 key,删中间项时后面的 DOM 节点被复用、只改文本,库认为“没变化”,结果是文字瞬间换了但位置没动。第二种是用随机数当 key,每次渲染都生成新 key,框架会把所有节点全删了重建,库看到的就是“全部离场 + 全部入场”,屏幕上一片闪烁。第三种是后端返回的数据里
id
在列表刷新时会变(比如用时间戳),同样会导致全量重建。
判断方法很直接:打开浏览器开发者工具的“元素”面板,操作一次,看 DOM 节点的数量是不是整批变化。整批变化就是 key 的问题,和库无关。修法就是老老实实用业务上稳定的唯一标识;实在没有唯一标识的场景,自己在数据进前端时生成一次 UUID 并挂到数据对象上,别在渲染时生成。
5.3 与自身
transition
打架
元素自己身上如果已经有
transition: all 0.3s
,再加上库的 WAAPI 动画,两者会同时生效,表现为动画幅度变大、时长变长、结束后还可能抖一下。因为
transition
遇到属性变化会自动插值,而 WAAPI 又在外面叠加了一层插值。
处理方式是把职责分开:
位置和显隐交给库,颜色、阴影、边框这类非几何属性交给 CSS
transition
。不要用
transition: all
,改成明确列属性。我在做卡片悬停效果时就是这么切的,悬停的阴影和边框走 CSS,位置变化走库,互不干扰。
还有一个相关的:如果元素自身带
transform
(比如一个角度标签、一个缩放的小徽章),它的
transform
会和动画的
transform
互相覆盖。稳妥做法是给这类元素外面包一层容器,让库只操作外层,内层的
transform
自娱自乐。
5.4 长列表的性能边界
它的性能模型是“每次变化全量比对当前子元素”。100 个以内的列表基本无感;300 个以上、并且频繁增删时,就需要注意了,因为每次变化都要对所有子元素读一遍矩形框,这本身是有成本的。虽然读取是批量的,但数量在那里摆着。
我的优化顺序是:先用分页或虚拟化把渲染量压下去(虚拟化场景本身不适合这个库,那就干脆别用);再按
duration
缩短高频操作的时间;最后考虑用插件的
onMove
钩子按位移距离动态调整时长——位移小的快速完成,位移大的给足时间,观感反而更利落。这个技巧我用了之后,长列表的“拖泥带水”感明显减轻。
5.5 排查速查表
| 现象 | 最可能的原因 | 处理动作 |
|---|---|---|
| 完全没动画 | 挂错了元素层级(挂到孙子层) | 挂到直接产生子节点增删的那一层 |
| 有动画但方向反了 | 坐标系被滚动或祖先变换干扰 |
检查滚动容器与
transform
祖先
|
| 删了但元素没动,只换了文字 | key 不稳定,DOM 节点被复用 | 换成业务唯一 ID |
| 动画结束后位置跳一下 | 动画期间布局被异步内容改变 | 给元素确定尺寸,避免图片/异步内容撑开 |
| 整屏闪烁 | 全量重建(key 每次都变) | 固定 key,避免随机值 |
| 动画幅度比预期大 |
与元素自身的
transition
叠加
|
去掉
transition: all
,改用显式属性
|
| 拖拽时其他元素乱抖 | 拖拽过程频繁移动节点 |
拖拽期间
disable()
,落位后
enable()
|
| 高度突变 | 容器高度依赖内容,且未设约束 | 给容器最小高度或固定高度 |
6. 插件机制:把“一刀切”变成“分场景”
6.1 三个生命周期钩子
库对外暴露了一个插件入口,插件函数拿到元素,返回一个带钩子的对象,钩子有三个:元素进入、元素离开、元素移动。这三个时机正好对应你可能想“插手”的三个点。
拿它干什么最有价值?我的用法是按元素在列表里的位置做错峰:进入时如果元素索引靠后,就加一点延迟,让整批元素像波浪一样依次出现,而不是齐刷刷同时冒出来。这个效果不需要写时间轴,只需要在钩子里读一下
index
然后设置延迟。
const wavePlugin = () => ({
onEnter: (el) => {
const index = Array.prototype.indexOf.call(el.parentElement.children, el)
// 索引越大,延迟越多,形成依次出现的节奏
el.animate(
[{ opacity: 0, transform: 'scale(0.96)' }, { opacity: 1, transform: 'none' }],
{ duration: 220, delay: Math.min(index, 8) * 30, easing: 'ease-out', fill: 'backwards' }
)
},
})
autoAnimate(listEl, { plugins: [wavePlugin] })
离场钩子常用来做“反向波浪”,或者在被删元素上留个淡出标记。移动钩子用得少一些,我一般在里面做位移距离的读取,配合动态时长使用。
6.2 封装成团队内部的通用组件
这个库的 API 太薄,直接在业务里到处
useAutoAnimate()
,过两个月没人记得哪些地方调了什么参数。我习惯封一层薄组件,把动画参数、系统动效偏好、性能降级策略都收进去。
// 统一入口:默认参数、统一降级、统一日志
export function MotionList({ as: Tag = 'ul', config = MOTION.listItem, children, ...rest }) {
const [parent] = useAutoAnimate(config)
return <Tag ref={parent} {...rest}>{children}</Tag>
}
封装时值得加的一层是
降级开关
。用一个全局配置控制是否启用动画,遇到低端设备、无障碍需求、或者需要截图对比的测试环境,一键关掉。实现上不是传
duration: 0
(那还是会跑动画,只是看不见),而是不挂 ref,或者调用控制器的
disable()
。这个开关在做性能排查时特别好用——关掉动画后问题还在,说明锅不在动画层。
再往上一层,可以做一个“动效策略表”,把不同容器类型映射到不同参数,业务侧只需要声明自己是哪一类,不关心具体毫秒数。设计稿改一次时长,改一处配置就行,不会出现“这个列表 200ms、那个列表 250ms、第三个 300ms”的历史遗留。
注意:插件里如果调用了
element.animate(),记得处理fill模式。用fill: 'backwards'能让延迟期间保持首帧状态,避免元素先闪一下真实样式再开始动画;用forwards则要小心动画结束后状态被锁死,后续手动改样式不生效。
7. 最后聊几条实打实的心得
先说一个反直觉的结论: 这个库最大的价值不是“加动画”,是“让状态变化可追踪” 。列表重排时,用户靠动画轨迹判断哪一项去了哪里;表单字段增加时,用户靠新元素的出现位置确认自己点对了按钮。这些是信息传达,不是装饰。所以我在做取舍时,宁可把时长调短一点、幅度调小一点,也要保证每一项变化都有明确的视觉交代——反过来,如果某个动画让人看不清发生了什么,那它就是在扣分,直接删掉比留着强。
第二条是关于“不做什么”。我见过不少项目,列表项进入时要有弹性缩放、离场时要旋转淡出、重排时还要加上依次延迟,最后整页看起来像在打鼓。这个库的设计取向是克制,它的默认参数就是 250ms 加
ease-in-out
,几乎不加任何冗余动作。跟着它的调性走反而效果最稳:一个页面里保留一到两处明显的动效,其余全部压到“隐约可察觉”的程度,层次感就出来了。
第三条是测试环节。自动过渡动画这类东西,自动化测试很难覆盖,我的土办法是固定几个典型的操作序列——删中间项、删首项、批量筛选、展开收起——每次改完动效配置就手动走一遍,同时开浏览器的动效录制看关键帧。录制回放是排查位置偏移最快的手段,比读代码强,因为你能直接看到元素是从哪个坐标飞过来的,而这个坐标往往就是问题所在。
如果你准备在现有项目里引入它,我给一个最小风险的落地顺序:先挑一个列表页试点,只做增删不做排序;观察两周,确认没有和现有样式打架;再推广到筛选和排序场景;最后才考虑动态表单这类结构更复杂的地方。一处一处来,比一次性铺开要稳得多。
AutoAnimate 是一个专为 DOM 结构变化(如子节点增删、重排)提供自动过渡动画的轻量级库。它基于 MutationObserver 监听直接子节点变化,利用文档坐标计算位置差,并通过 Web Animations API 实现位置、缩放与透明度的平滑过渡。该方案不处理内容切换、高度变化或虚拟滚动等非结构变更场景,适用于后台列表、动态表单、折叠面板等强调状态可追踪性的中后台应用,兼顾性能、可访问性与开发效率。
479

被折叠的 条评论
为什么被折叠?



