news 2026/9/10 10:15:47

airi 项目 Vue 3 列表动画实践:TransitionGroup 组件最佳实践与源码级实现解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
airi 项目 Vue 3 列表动画实践:TransitionGroup 组件最佳实践与源码级实现解析

airi 项目 Vue 3 列表动画实践:TransitionGroup 组件最佳实践与源码级实现解析

【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi

导读

<TransitionGroup>是 Vue 3 内置的列表过渡组件,专门用于处理一组子元素「进入、离开、移动」三类动画。本文以 component-transition-group.md 最佳实践指南为骨架,逐条讲解其使用边界、key规则、tag包装、mode禁用约束与基于 JavaScript 钩子的交错(stagger)动画,并结合 airi 仓库中 double-check-button.vue 与 beat-sync.vue 的真实实现,帮助你在自己的 Vue 3 应用中写出稳定、流畅、可维护的列表过渡。读完你既能避开最常见的踩坑点,也能看到生产级列表动画的落地写法。

适用范围:什么是 TransitionGroup,以及何时使用它

Vue 3 提供了两套内置过渡原语,选择依据非常明确:

  • <Transition>:针对单个元素/组件的进入与离开(enter/leave),可在单元素上应用mode="out-in"/mode="in-out"做进出时序编排;
  • <TransitionGroup>:针对v-for渲染的一组列表项,除了进入、离开外,还额外支持移动(move)动画——当列表增删、排序导致兄弟节点相对位置变化时,Vue 会用 FLIP 思路平滑过渡。

实践指南给出一份直接可执行的任务清单,是使用<TransitionGroup>时的默认检查项:

  • 只在列表与重复项场景使用<TransitionGroup>
  • 为每个直接子元素提供唯一且稳定的key
  • 需要语义化或布局类容器时通过tag指定包装元素;
  • 不要使用modeprop(它不被支持);
  • 交错(stagger)效果请用 JavaScript 钩子实现。

airi 仓库中每个 Vue 应用都遵循 Vue 3 + Composition API +<script setup>+ TypeScript 规范(见 vue-best-practices SKILL.md),而技能库在“动画相关特性”一节也将<TransitionGroup>定位为“animated list mutations(列表变更动画)”的唯一内置选项,与 component-transition.md(单元素过渡)、class-based / state-driven 动画技术互为补充。换句话说,遇到“列表项会增删或重排”的需求时,首选就是<TransitionGroup>

只为列表使用 TransitionGroup

<TransitionGroup>是为重复渲染的列表项设计的,不应把毫无关系的固定兄弟组件塞进它。此时即使写了name,也不会得到预期动画,反而引入不必要的层级与约束。

错误示范(把两个固定组件当作过渡子项):

<template> <TransitionGroup name="fade"> <ComponentA /> <ComponentB /> </TransitionGroup> </template>

正确示范tag="ul"决定真实包装标签,v-for列表作为子项):

<template> <TransitionGroup name="list" tag="ul"> <li v-for="item in items" :key="item.id"> {{ item.name }} </li> </TransitionGroup> </template>

关于tag属性需要补两点实现细节:

  1. 在 Vue 3 中,tag已不是必填项——不传时<TransitionGroup>不会渲染多余的包裹元素,直接输出子节点列表;但当需要语义化标签(ul/ol/table)或需要一个确定布局的容器(如div+ flex/grid)时,显式传入tag是标准做法,上述真实实现也都传入了tag="div"
  2. 传入的tag元素上可以继续绑定 class/style 与普通 HTML 属性,Vue 会统一挂载到该包装元素上(attribute 继承规则仍然适用)。

仓库中的 double-check-button.vue 是一个“固定少数子项”的实战变体:它用<TransitionGroup name="double-check-slide" tag="div">包裹两个带显式key的按钮(key="cancel"key="primary"),配合v-if="confirming"让取消按钮在确认模式下出现/消失,从而触发主按钮被“挤开”的移动动画。这说明<TransitionGroup>不必局限于超长列表,两个带 key 的兄弟元素构成的小型动态集合同样适用——关键在于子项必须是被key标识、可能进入或离开视图的成员,而不是互不相干的静态组件。

始终提供稳定且唯一的 key

key<TransitionGroup>的硬性要求。没有稳定的 key,Vue 无法在多次渲染间追踪每个元素的身份:位置无法精确对应,移动动画自然失效,增删元素时还会发生复用错乱。

错误示范(用数组下标作 key):

<template> <TransitionGroup name="list" tag="ul"> <li v-for="(item, index) in items" :key="index"> {{ item.name }} </li> </TransitionGroup> </template>

正确示范(用业务主键作 key):

<template> <TransitionGroup name="list" tag="ul"> <li v-for="item in items" :key="item.id"> {{ item.name }} </li> </TransitionGroup> </template>

为什么下标 key 是灾难:以 index 为 key 时,元素身份跟着“位置”走而非跟着“数据”走。插入或删除头部元素会让后续所有元素的身份整体平移,Vue 会认为它们是被替换而非移动,导致 DOM 复用、过渡动画全部错位。选择 key 时应遵守:

  • 唯一:同一次渲染内互不重复;
  • 稳定:跨多次渲染保持不变(与业务实体绑定,如数据库 id、自增序号);
  • 不被复用:删除后不再出现(避免使用“临时自增计数器”这种会增长的 key,会造成数组收缩时 key 错位)。

这一条与 Vue 通用列表渲染规范一致,也是仓库 vue-best-practices 中“模板安全(list rendering)”要求的一部分。

不要在 TransitionGroup 上使用 mode

mode="out-in"/mode="in-out"只属于<Transition>:它用于在同一时刻只能渲染一个元素、需要控制“旧元素离开后再进入新元素”的场景。而<TransitionGroup>的子元素是同时存在的,根本没有“替换单个元素”的语义,因此不提供mode——传了会被静默忽略,且并不会产生任何进/出排序效果。

错误示范(对组施加 mode,无效且产生误解):

<template> <TransitionGroup name="list" tag="div" mode="out-in"> <div v-for="item in items" :key="item.id">{{ item.name }}</div> </TransitionGroup> </template>

正确示范(单元素切换的进出编排请用<Transition>,并通过:key强制视图重建):

<template> <Transition name="fade" mode="out-in"> <component :is="currentView" :key="currentView" /> </Transition> </template>

实现依据:mode之所以能起作用,是因为<Transition>内部维护“旧元素销毁 / 新元素挂载”两个阶段;而<TransitionGroup>只关心同一集合内成员的增删移,渲染层并不存在“二选一”的时刻。如果你观察仓库中 double-check-button.vue 的“按钮互换”,会发现它并未依赖mode,而是用v-if+ 稳定 key 让两个按钮在集合中增删,再借助移动过渡与“离场元素脱离布局流”的 CSS 技巧(见后文“move 过渡与离场脱离布局流”)完成平滑换位——这正是 group 语义下实现进出编排的正确姿势。

用 JavaScript 钩子实现交错(stagger)动画

当需要“瀑布式”逐条入场时,<TransitionGroup>的 CSS class 方案不够灵活。最佳实践指南给出的标准做法是:关闭 CSS 控制(:css="false"),在@before-enter/@enter钩子中读取子项携带的索引,为每个元素计算不同的延迟。

原文档提供的完整示例:

<template> <TransitionGroup tag="ul" :css="false" @before-enter="onBeforeEnter" @enter="onEnter" > <li v-for="(item, index) in items" :key="item.id" :data-index="index"> {{ item.name }} </li> </TransitionGroup> </template> <script setup> function onBeforeEnter(el) { el.style.opacity = 0 el.style.transform = 'translateY(12px)' } function onEnter(el, done) { const delay = Number(el.dataset.index) * 80 setTimeout(() => { el.style.transition = 'all 0.25s ease' el.style.opacity = 1 el.style.transform = 'translateY(0)' setTimeout(done, 250) }, delay) } </script>

逐行拆解这套写法背后的契约:

  • :css="false":告知 Vue 不再依赖 CSS 类判断过渡何时结束,enter钩子必须显式调用done()来声明动画完成,否则该元素会被认为永远处于过渡中;
  • :data-index="index":把索引以 data 属性挂在元素上,钩子通过el.dataset.index读取——这是“模板只负责声明、逻辑负责计算延迟”的干净解耦;
  • onBeforeEnter设置初始状态(透明、向下偏移),onEnterindex * 80ms递增延迟后播放到目标态,并在自身动画结束后setTimeout(done, 250)通知 Vue 收尾;
  • 为了视觉自然,延迟只应施加于入场路径;若同时需要离场交错,可继续在@leave钩子中做反向处理。

airi 仓库中 beat-sync.vue 的“节拍可视化涟漪”模块正是这一模式的工程化版本。它使用:css="false"只监听@enter="onRippleEnter",为每个涟漪圆点渲染<div v-for="beat in beatsHistory" :key="beat.id" :data-beat-id="beat.id" />

<TransitionGroup tag="div" ... :css="false" @enter="onRippleEnter" > <div v-for="beat in beatsHistory" :key="beat.id" :data-beat-id="beat.id" ... /> </TransitionGroup>

对应的 onRippleEnter 从元素上取回业务标识el.dataset.beatId,用基于时间线的动画库让圆点从scale: 0, opacity: 1扩散到scale: 1, opacity: 0,在onComplete回调里找到对应项并从响应式数组beatsHistorysplice移除,最后调用done()完成整个过渡周期:

function onRippleEnter(el: Element, done: () => void) { const beatId = (el as HTMLElement).dataset.beatId createTimeline() .set(el, { opacity: 1, scale: 0 }) .add(el, { opacity: 0, scale: 1, duration: 2000, ease: 'out(5)', onComplete: () => { if (!beatId) return const idx = beatsHistory.value.findIndex(b => b.id === beatId) if (idx >= 0) beatsHistory.value.splice(idx, 1) done() }, }) }

这个实现值得学习的两点设计:

  1. 数据驱动生命周期:动画结束后不是等待外部定时清空,而是在done()之前同步从数组中移除该项——元素从 v-for 数据中被删除,DOM 才真正退场,动画与数据状态天然一致;
  2. dataset 传参:与文档示例的:data-index思路相同,把渲染期才可知的身份(id/index)通过 data 属性传入 JS 钩子,避免在闭包里捕获过期索引导致错删。

move 过渡与离场元素脱离布局流

进入/离开之外,<TransitionGroup>的独有能力是移动动画:当列表重排或某个元素离开导致其他成员位移时,Vue 会为每个“换了位置”的元素加上<name>-move类,你可以用它声明 transform 过渡,实现 FLIP 般的平滑滑动。

从 double-check-button.vue 的样式可以看到一个关键组合:

.double-check-slide-move { transition: transform 180ms ease; } .double-check-slide-enter-active, .double-check-slide-leave-active { transition: opacity 160ms ease, transform 160ms ease; } /* 让离场按钮脱离 flex 布局流,主按钮的位移才能被 TransitionGroup 捕获为 move 动画 */ .double-check-slide-leave-active { position: absolute; } .double-check-slide-enter-from, .double-check-slide-leave-to { opacity: 0; transform: translateX(-10px); }

其中.double-check-slide-leave-active { position: absolute }是让 move 动画生效的经典技巧:默认情况下,正在“离开”的元素仍占据文档流空间,其余兄弟节点不会真正位移,FLIP 也就无从谈起;把它设为absolute后,离场元素被抽出布局流,主按钮的重新排布才成为一次可观测的“移动”,配合-move过渡产生顺滑的让位效果。仓库注释也明确说明了这一点:“Removing the cancel button from flex flow lets TransitionGroup animate the primary button's layout shift.”

因此一个完整的<TransitionGroup>过渡通常需要四段 CSS 声明:

CSS 类作用触发时机
<name>-enter-from/<name>-enter-active/<name>-enter-to元素进入动画新增子项时
<name>-leave-from/<name>-leave-active/<name>-leave-to元素离开动画删除子项时
<name>-move位置迁移动画(一般只写 transition)兄弟节点因增删/重排而位移时
-leave-active { position: absolute }将离场元素移出布局流,使 move 动画可见需要让位动画时

常见误区速查与相关参考

把本文所有约束收敛为一张自检表,可作为评审列表动画代码时的 check-list:

  1. 是否把固定、无关的兄弟组件放进了<TransitionGroup>?——应只包裹会增删移的集合子项;
  2. 每个直接子项是否有keykey是否绑定业务唯一标识而非 index?——下标 key 会破坏移动追踪;
  3. 是否给<TransitionGroup>传了mode?——它只适用于<Transition>
  4. 需要列表之外的单元素进出编排?——改用<Transition mode="out-in">并配合:key切换视图;
  5. 逐条入场是否有延迟控制?——用:css="false"+ JS 钩子 + dataset 索引/标识;
  6. 删除后其余项是否“瞬移”而非滑动?——检查是否有<name>-move过渡,以及离场元素是否需要position: absolute脱离布局流;
  7. JS 钩子模式下是否每个分支都调用了done()?——漏调会导致元素悬挂在过渡态。

想继续深入相邻主题,仓库内可直接阅读以下资源:

  • 技能库动画决策:<.agents/skills/vue-best-practices/SKILL.md>(其中明确列出动画四选一的选型流程);
  • 单元素过渡配套指南:component-transition.md;
  • 非进出场动画:class-based 与 state-driven 两类技术的参考文档见 references 目录;
  • 生产级实现样例:double-check-button.vue(多子项 + move 让位)与 beat-sync.vue(:css="false"+ JS 钩子动画循环);
  • 注意区分命名陷阱:仓库 ui-transitions 包 中导出的StageTransitionGroup是整套页面级路由转场编排器(配合 vue-router 的beforeEach与生命周期钩子系统运行),并非 Vue 内置<TransitionGroup>的封装——当你在依赖中看到以 Group 命名的组件时,务必先确认它到底是“列表过渡”还是别的过渡编排,避免误用。

【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi

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

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

CANN/GE AIPP色域转换API

aclmdlSetAIPPCscParams 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、Te…

作者头像 李华
网站建设 2026/9/10 10:14:36

沉浸式翻译云同步完整教程:3步搞定多设备不再重配

沉浸式翻译云同步完整教程&#xff1a;3步搞定多设备不再重配 【免费下载链接】immersive-translate 沉浸式双语网页翻译扩展 , 支持输入框翻译&#xff0c; 鼠标悬停翻译&#xff0c; PDF, Epub, 字幕文件, TXT 文件翻译 - Immersive Dual Web Page Translation Extension …

作者头像 李华
网站建设 2026/9/10 10:13:54

用AD从零开始画pcb

入门Altium Designer&#xff0c;从0开始画PCB&#xff0c;我是按照下面这个教程入门的&#xff1a; 软件下载&#xff1a;吴川斌的博客 公众号搜索下载。 Altium Designer 教程(一)——序 - 知乎 (zhihu.com) Altium Designer 教程(二)——软件基本功能 - 知乎 (zhihu.com) …

作者头像 李华
网站建设 2026/9/10 10:13:17

超帧技术:实时音频处理中的频谱分辨率与降噪工程实践

1. 为什么需要超帧&#xff1a;从单帧处理聊起做实时音频处理的朋友应该都有体会&#xff0c;不管你是写降噪算法、回声消除还是语音增强&#xff0c;第一步基本逃不开分帧加窗、逐帧处理这条路。以16kHz采样率为例&#xff0c;常见的帧长是20ms到32ms&#xff0c;也就是320到5…

作者头像 李华
网站建设 2026/9/10 10:13:04

CANN/GE获取张量元素个数接口

aclGetTensorDescElementCount 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTor…

作者头像 李华
网站建设 2026/9/10 10:10:38

Tracy Profiler 五分钟上手:告别玄学掉帧,精准定位性能瓶颈

Tracy Profiler 五分钟上手&#xff1a;告别玄学掉帧&#xff0c;精准定位性能瓶颈 【免费下载链接】tracy Frame profiler 项目地址: https://gitcode.com/GitHub_Trending/tr/tracy 昨晚测试同学反馈游戏每 30 秒帧率从 60 掉到 30&#xff0c;本地却复现不了。打日志…

作者头像 李华