Vue3 动画与过渡
Vue3 的 Transition 和 TransitionGroup 组件让界面交互更加生动,从 CSS 类名变化到 JavaScript 钩子,全面掌握动画系统的每一个细节。
一、前言
动画不仅是视觉装饰,更是提升用户体验的重要手段。良好的过渡动画可以:
- 帮助用户理解界面状态的变化
- 缓解等待焦虑(加载动画)
- 引导用户注意力
- 增强界面的专业感和精致度
Vue3 继承了 Vue2 的过渡系统,并进行了优化和调整。本文将系统讲解 Vue3 中的动画与过渡,包括基础用法、JavaScript 钩子、列表动画以及与第三方库的集成。
二、Transition 组件
2.1 基本用法
<Transition>是 Vue3 内置的过渡组件,包裹元素或组件后,在插入、更新或移除 DOM 时自动应用过渡类名。
<template> <button @click="show = !show">切换显示</button> <Transition> <p v-if="show" class="greeting">你好,Vue3!</p> </Transition> </template> <script setup> import { ref } from 'vue' const show = ref(true) </script> <style scoped> /* 进入动画 */ .v-enter-active { transition: opacity 0.5s ease; } .v-enter-from { opacity: 0; } .v-enter-to { opacity: 1; } /* 离开动画 */ .v-leave-active { transition: opacity 0.5s ease; } .v-leave-from { opacity: 1; } .v-leave-to { opacity: 0; } </style>2.2 Vue3 过渡类名变化
Vue3 对过渡类名进行了调整,这是与 Vue2 最大的区别之一:
| 状态 | Vue2 类名 | Vue3 类名 | 说明 |
|---|---|---|---|
| 进入开始 | v-enter | v-enter-from | 元素插入前的初始状态 |
| 进入中 | v-enter-active | v-enter-active | 整个进入过渡阶段 |
| 进入结束 | v-enter-to | v-enter-to | 元素插入后的最终状态 |
| 离开开始 | v-leave | v-leave-from | 元素移除前的初始状态 |
| 离开中 | v-leave-active | v-leave-active | 整个离开过渡阶段 |
| 离开结束 | v-leave-to | v-leave-to | 元素移除后的最终状态 |
关键变化:Vue3 将v-enter重命名为v-enter-from,将v-leave重命名为v-leave-from,使命名更加语义化。
2.3 自定义过渡类名
当需要集成第三方 CSS 动画库(如 Animate.css)时,可以使用自定义类名:
<template> <Transition enter-active-class="animate__animated animate__bounceIn" leave-active-class="animate__animated animate__bounceOut" > <p v-if="show" class="animate__animated">Animate.css 动画</p> </Transition> </template> <script setup> import { ref } from 'vue' const show = ref(true) </script> <style> /* 引入 Animate.css */ @import 'animate.css'; </style>2.4 命名过渡
通过name属性可以自定义过渡类名的前缀:
<template> <Transition name="fade"> <p v-if="show">命名过渡</p> </Transition> </template> <script setup> import { ref } from 'vue' const show = ref(true) </script> <style scoped> /* 使用 fade- 前缀 */ .fade-enter-active, .fade-leave-active { transition: opacity 0.5s ease; } .fade-enter-from, .fade-leave-to { opacity: 0; } </style>2.5 CSS 过渡 vs CSS 动画
Vue 的过渡系统同时支持 CSS 过渡(transition)和 CSS 动画(animation):
<style scoped> /* CSS 过渡方式 */ .slide-enter-active { transition: transform 0.3s ease; } .slide-enter-from { transform: translateX(-100%); } /* CSS 动画方式 */ .bounce-enter-active { animation: bounce-in 0.5s; } .bounce-leave-active { animation: bounce-in 0.5s reverse; } @keyframes bounce-in { 0% { transform: scale(0); } 50% { transform: scale(1.25); } 100% { transform: scale(1); } } </style>三、TransitionGroup 组件
3.1 列表过渡基础
<TransitionGroup>用于对v-for列表进行过渡动画:
<template> <div> <button @click="addItem">添加项目</button> <button @click="shuffle">打乱顺序</button> <TransitionGroup name="list" tag="ul"> <li v-for="item in items" :key="item.id"> {{ item.text }} <button @click="removeItem(item.id)">删除</button> </li> </TransitionGroup> </div> </template> <script setup> import { ref } from 'vue' const items = ref([ { id: 1, text: '项目 1' }, { id: 2, text: '项目 2' }, { id: 3, text: '项目 3' } ]) let nextId = 4 const addItem = () => { items.value.push({ id: nextId++, text: `项目 ${nextId - 1}` }) } const removeItem = (id) => { const index = items.value.findIndex(item => item.id === id) if (index > -1) { items.value.splice(index, 1) } } const shuffle = () => { items.value = items.value.sort(() => Math.random() - 0.5) } </script> <style scoped> .list-enter-active, .list-leave-active { transition: all 0.5s ease; } .list-enter-from, .list-leave-to { opacity: 0; transform: translateX(30px); } /* 确保离开的元素脱离文档流,其他元素可以平滑移动 */ .list-leave-active { position: absolute; } /* 列表项移动时的过渡 */ .list-move { transition: transform 0.5s ease; } </style>3.2 FLIP 动画原理
TransitionGroup 内部使用了FLIP动画技术:
- First:记录元素的初始位置
- Last:记录元素的最终位置
- Invert:计算并应用反向偏移
- Play:播放过渡动画
TransitionGroup 会自动处理这个过程,开发者只需定义.v-move类即可。
3.3 列表排序动画
<template> <div class="todo-app"> <div class="filters"> <button @click="sortBy = 'default'">默认</button> <button @click="sortBy = 'priority'">按优先级</button> <button @click="sortBy = 'date'">按日期</button> </div> <TransitionGroup name="todo" tag="div" class="todo-list"> <div v-for="todo in sortedTodos" :key="todo.id" class="todo-item" :class="`priority-${todo.priority}`" > <span>{{ todo.title }}</span> <span>{{ todo.date }}</span> </div> </TransitionGroup> </div> </template> <script setup> import { ref, computed } from 'vue' const sortBy = ref('default') const todos = ref([ { id: 1, title: '完成项目文档', priority: 'high', date: '2024-01-15' }, { id: 2, title: '修复 Bug', priority: 'medium', date: '2024-01-10' }, { id: 3, title: '代码审查', priority: 'low', date: '2024-01-20' }, { id: 4, title: '部署上线', priority: 'high', date: '2024-01-12' } ]) const sortedTodos = computed(() => { const list = [...todos.value] switch (sortBy.value) { case 'priority': const priorityMap = { high: 3, medium: 2, low: 1 } return list.sort((a, b) => priorityMap[b.priority] - priorityMap[a.priority]) case 'date': return list.sort((a, b) => new Date(a.date) - new Date(b.date)) default: return list } }) </script> <style scoped> .todo-list { position: relative; } .todo-item { padding: 12px; margin: 8px 0; border-radius: 6px; background: #f5f5f5; display: flex; justify-content: space-between; align-items: center; } .priority-high { border-left: 4px solid #f44336; } .priority-medium { border-left: 4px solid #ff9800; } .priority-low { border-left: 4px solid #4caf50; } .todo-enter-active, .todo-leave-active { transition: all 0.4s ease; } .todo-enter-from, .todo-leave-to { opacity: 0; transform: scale(0.9); } .todo-leave-active { position: absolute; width: 100%; } .todo-move { transition: transform 0.4s ease; } </style>四、JavaScript 钩子
4.1 完整的钩子函数
当 CSS 动画无法满足需求时,可以使用 JavaScript 钩子进行精细控制:
<template> <Transition @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter" @enter-cancelled="enterCancelled" @before-leave="beforeLeave" @leave="leave" @after-leave="afterLeave" @leave-cancelled="leaveCancelled" :css="false" > <div v-if="show" class="js-animated-box"> JavaScript 动画控制 </div> </Transition> </template> <script setup> import { ref } from 'vue' const show = ref(true) // 进入前 const beforeEnter = (el) => { el.style.opacity = '0' el.style.transform = 'scale(0)' } // 进入中 const enter = (el, done) => { // 使用 Web Animations API const animation = el.animate([ { opacity: 0, transform: 'scale(0)' }, { opacity: 1, transform: 'scale(1)' } ], { duration: 500, easing: 'ease-out' }) animation.onfinish = () => done() } // 进入完成 const afterEnter = (el) => { console.log('进入动画完成') } // 进入取消 const enterCancelled = (el) => { console.log('进入动画被取消') } // 离开前 const beforeLeave = (el) => { el.style.opacity = '1' } // 离开中 const leave = (el, done) => { const animation = el.animate([ { opacity: 1, transform: 'scale(1)' }, { opacity: 0, transform: 'scale(0)' } ], { duration: 300, easing: 'ease-in' }) animation.onfinish = () => done() } // 离开完成 const afterLeave = (el) => { console.log('离开动画完成') } // 离开取消 const leaveCancelled = (el) => { console.log('离开动画被取消') } </script>4.2 与 GSAP 集成
GSAP 是一个强大的 JavaScript 动画库,与 Vue 的过渡系统配合使用效果极佳:
<template> <Transition @enter="onEnter" @leave="onLeave" :css="false" appear > <div v-if="show" class="gsap-box"> GSAP 动画 </div> </Transition> </template> <script setup> import { ref } from 'vue' import gsap from 'gsap' const show = ref(true) const onEnter = (el, done) => { gsap.fromTo(el, { opacity: 0, y: 50, scale: 0.8 }, { opacity: 1, y: 0, scale: 1, duration: 0.6, ease: 'back.out(1.7)', onComplete: done } ) } const onLeave = (el, done) => { gsap.to(el, { opacity: 0, y: -50, scale: 0.8, duration: 0.4, ease: 'power2.in', onComplete: done }) } </script> <style scoped> .gsap-box { padding: 40px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 12px; text-align: center; font-size: 18px; } </style>五、性能优化
5.1 GPU 加速
使用transform和opacity属性触发 GPU 加速,避免引起重排(reflow):
/* 推荐:使用 transform 和 opacity */.slide-enter-active{transition:transform 0.3s ease,opacity 0.3s ease;}.slide-enter-from{transform:translateX(100%);opacity:0;}/* 避免:使用 width、height、top、left 等属性 *//* 这些属性会引起重排,性能较差 */.bad-example-enter-active{transition:width 0.3s ease;/* 不推荐 */}5.2 will-change 属性
对于复杂的动画,可以预先告知浏览器哪些属性将发生变化:
.animated-element{will-change:transform,opacity;}/* 动画完成后移除 will-change */.animated-element.animation-complete{will-change:auto;}注意:will-change会占用额外的内存,动画结束后应及时移除。
5.3 减少同时动画的元素数量
当列表中有大量元素需要同时动画时,考虑分批处理或使用虚拟列表:
<script setup> import { ref, computed } from 'vue' const items = ref([...Array(100).keys()]) // 分批显示动画 const visibleItems = computed(() => { // 只显示前 20 个,或根据滚动位置动态计算 return items.value.slice(0, 20) }) </script>六、Vue2 vs Vue3 过渡对比
| 特性 | Vue2 | Vue3 |
|---|---|---|
| 进入开始类名 | v-enter | v-enter-from |
| 离开开始类名 | v-leave | v-leave-from |
| 进入结束类名 | v-enter-to | v-enter-to |
| 离开结束类名 | v-leave-to | v-leave-to |
| TransitionGroup 标签 | 默认span | 默认span |
| TransitionGroup tag 属性 | 支持 | 支持 |
| appear 属性 | 支持 | 支持 |
| mode 属性 | in-out/out-in | in-out/out-in |
| CSS 钩子 | 支持 | 支持 |
| JS 钩子 | 支持 | 支持,增加了onLeaveCancelled |
| 类型支持 | 有限 | 完整的 TypeScript 支持 |
七、实战:完整的页面过渡系统
<!-- App.vue --> <template> <div id="app"> <nav> <RouterLink to="/">首页</RouterLink> <RouterLink to="/about">关于</RouterLink> <RouterLink to="/list">列表</RouterLink> </nav> <RouterView v-slot="{ Component }"> <Transition name="page" mode="out-in"> <component :is="Component" /> </Transition> </RouterView> </div> </template> <style> /* 页面过渡 */ .page-enter-active, .page-leave-active { transition: opacity 0.3s ease, transform 0.3s ease; } .page-enter-from { opacity: 0; transform: translateX(20px); } .page-leave-to { opacity: 0; transform: translateX(-20px); } /* 路由激活状态 */ .router-link-active { color: #42b983; font-weight: bold; } </style>八、常见问题
Q1:为什么我的过渡动画不生效?
常见原因:
- 忘记给
v-for的元素添加唯一的:key - CSS 类名写错(Vue3 使用
v-enter-from而非v-enter) - 过渡元素没有条件渲染(
v-if或v-show) - CSS 选择器优先级不够
Q2:TransitionGroup 的 move 动画不生效?
确保:
- 设置了
.v-move类 - 列表项有唯一的
:key - 离开的元素设置了
position: absolute
Q3:如何在组件首次渲染时触发动画?
使用appear属性:
<Transition appear> <div>首次渲染也会触发动画</div> </Transition>Q4:mode=“out-in” 和 mode=“in-out” 有什么区别?
out-in:当前元素先离开,新元素再进入(推荐,避免两个元素同时存在)in-out:新元素先进入,当前元素再离开
九、总结
Vue3 的过渡系统功能强大且易于使用:
| 组件 | 用途 | 关键特性 |
|---|---|---|
| Transition | 单元素/组件过渡 | 进入/离开类名、JS 钩子、mode |
| TransitionGroup | 列表过渡 | move 动画、tag 属性、FLIP |
核心要点:
- Vue3 将
v-enter改为v-enter-from,v-leave改为v-leave-from - 优先使用
transform和opacity实现 GPU 加速 - TransitionGroup 需要唯一的
:key和.v-move类 - JavaScript 钩子适合与 GSAP 等库集成
- 合理使用
will-change和mode属性优化体验
十、练习题
实现一个手风琴(Accordion)组件,使用 Transition 实现展开/收起的平滑动画。
创建一个可拖拽排序的列表,使用 TransitionGroup 实现拖拽时的位置交换动画。
使用 GSAP 实现一个复杂的页面加载动画序列:Logo 先放大出现,然后标题从下方滑入,最后按钮淡入。