Vue 3 动画效果实现:Transition和TransitionGroup详解
前言
在现代Web应用中,流畅的动画效果不仅能提升用户体验,还能有效传达界面状态变化的信息。Vue 3 提供了强大的过渡和动画系统,通过 <transition> 和 <transition-group> 组件,开发者可以轻松地为元素的进入、离开和列表变化添加动画效果。本文将深入探讨这两个组件的使用方法和高级技巧。
Transition 组件基础
基本用法
<transition> 组件用于包装单个元素或组件,在插入、更新或移除时应用过渡效果。
vue
<template>
<div>
<button @click="show = !show">切换显示</button>
<transition name="fade">
<p v-if="show">Hello Vue 3!</p>
</transition>
</div>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(true)
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
过渡类名详解
Vue 3 为进入/离开过渡提供了6个CSS类名:
- v-enter-from:进入过渡的开始状态
- v-enter-active:进入过渡生效时的状态
- v-enter-to:进入过渡的结束状态
- v-leave-from:离开过渡的开始状态
- v-leave-active:离开过渡生效时的状态
- v-leave-to:离开过渡的结束状态
注意:在 Vue 3 中,类名前缀从
v-enter改为v-enter-from,其他类名也相应调整。
JavaScript 钩子函数
除了CSS过渡,还可以使用JavaScript钩子来控制动画:
vue
<template>
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
>
<div v-if="show" class="box">Animated Box</div>
</transition>
</template>
<script setup>
import { ref } from 'vue'
import gsap from 'gsap'
const show = ref(true)
const beforeEnter = (el) => {
el.style.opacity = 0
el.style.transform = 'scale(0)'
}
const enter = (el, done) => {
gsap.to(el, {
duration: 0.5,
opacity: 1,
scale: 1,
onComplete: done
})
}
const afterEnter = (el) => {
console.log('进入完成')
}
const beforeLeave = (el) => {
el.style.transformOrigin = 'center'
}
const leave = (el, done) => {
gsap.to(el, {
duration: 0.5,
opacity: 0,
scale: 0,
onComplete: done
})
}
const afterLeave = (el) => {
console.log('离开完成')
}
</script>
常见动画效果实现
1. 淡入淡出效果
vue
<template>
<div class="demo">
<button @click="show = !show">Toggle Fade</button>
<transition name="fade">
<div v-if="show" class="content">Fade Effect Content</div>
</transition>
</div>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease-in-out;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
2. 滑动效果
vue
<template>
<div class="demo">
<button @click="show = !show">Toggle Slide</button>
<transition name="slide">
<div v-if="show" class="content">Slide Effect Content</div>
</transition>
</div>
</template>
<style>
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s ease;
max-height: 200px;
overflow: hidden;
}
.slide-enter-from,
.slide-leave-to {
max-height: 0;
opacity: 0;
transform: translateY(-20px);
}
</style>
3. 弹跳效果
vue
<template>
<div class="demo">
<button @click="show = !show">Toggle Bounce</button>
<transition name="bounce">
<div v-if="show" class="content">Bounce Effect Content</div>
</transition>
</div>
</template>
<style>
.bounce-enter-active {
animation: bounce-in 0.5s;
}
.bounce-leave-active {
animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
opacity: 1;
}
}
</style>
4. 翻转效果
vue
<template>
<div class="demo">
<button @click="show = !show">Toggle Flip</button>
<transition name="flip">
<div v-if="show" class="content flip-content">Flip Effect Content</div>
</transition>
</div>
</template>
<style>
.flip-enter-active {
animation: flip-in 0.6s ease forwards;
}
.flip-leave-active {
animation: flip-out 0.6s ease forwards;
}
@keyframes flip-in {
0% {
transform: perspective(400px) rotateY(90deg);
opacity: 0;
}
40% {
transform: perspective(400px) rotateY(-10deg);
}
70% {
transform: perspective(400px) rotateY(10deg);
}
100% {
transform: perspective(400px) rotateY(0deg);
opacity: 1;
}
}
@keyframes flip-out {
0% {
transform: perspective(400px) rotateY(0deg);
opacity: 1;
}
100% {
transform: perspective(400px) rotateY(90deg);
opacity: 0;
}
}
</style>
TransitionGroup 组件详解
基本列表动画
<transition-group> 用于为列表中的元素添加进入/离开过渡效果:
vue
<template>
<div class="list-demo">
<button @click="addItem">添加项目</button>
<button @click="removeItem">删除项目</button>
<transition-group name="list" tag="ul">
<li v-for="item in items" :key="item.id" class="list-item">
{{ item.text }}
</li>
</transition-group>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const items = reactive([
{ id: 1, text: '项目 1' },
{ id: 2, text: '项目 2' },
{ id: 3, text: '项目 3' }
])
let nextId = 4
const addItem = () => {
const index = Math.floor(Math.random() * (items.length + 1))
items.splice(index, 0, {
id: nextId++,
text: `新项目 ${nextId - 1}`
})
}
const removeItem = () => {
if (items.length > 0) {
const index = Math.floor(Math.random() * items.length)
items.splice(index, 1)
}
}
</script>
<style>
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-move {
transition: transform 0.5s ease;
}
.list-item {
padding: 10px;
margin: 5px 0;
background-color: #f0f0f0;
border-radius: 4px;
}
</style>
列表排序动画
vue
<template>
<div class="shuffle-demo">
<button @click="shuffle">随机排序</button>
<button @click="add">添加</button>
<button @click="remove">删除</button>
<transition-group name="shuffle" tag="div" class="grid">
<div
v-for="item in items"
:key="item.id"
class="grid-item"
@click="removeItem(item)"
>
{{ item.number }}
</div>
</transition-group>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const items = reactive([
{ id: 1, number: 1 },
{ id: 2, number: 2 },
{ id: 3, number: 3 },
{ id: 4, number: 4 },
{ id: 5, number: 5 }
])
const shuffle = () => {
// Fisher-Yates 洗牌算法
for (let i = items.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[items[i], items[j]] = [items[j], items[i]]
}
}
const add = () => {
const newNumber = items.length > 0 ? Math.max(...items.map(i => i.number)) + 1 : 1
items.push({
id: Date.now(),
number: newNumber
})
}
const remove = () => {
if (items.length > 0) {
items.pop()
}
}
const removeItem = (item) => {
const index = items.indexOf(item)
if (index > -1) {
items.splice(index, 1)
}
}
</script>
<style>
.grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 20px;
}
.grid-item {
width: 60px;
height: 60px;
background-color: #42b883;
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
user-select: none;
}
.shuffle-enter-active,
.shuffle-leave-active {
transition: all 0.5s ease;
}
.shuffle-enter-from {
opacity: 0;
transform: scale(0.5);
}
.shuffle-leave-to {
opacity: 0;
transform: scale(0.5);
}
.shuffle-move {
transition: transform 0.5s ease;
}
</style>
高级动画技巧
1. FLIP 技术实现平滑动画
FLIP (First, Last, Invert, Play) 是一种优化动画性能的技术:
vue
<template>
<div class="flip-demo">
<button @click="filterItems">筛选奇数</button>
<button @click="resetFilter">重置</button>
<transition-group
name="flip-list"
tag="div"
class="flip-container"
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
>
<div
v-for="item in filteredItems"
:key="item.id"
class="flip-item"
>
{{ item.value }}
</div>
</transition-group>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const items = ref(Array.from({ length: 20 }, (_, i) => ({
id: i + 1,
value: i + 1
})))
const filterOdd = ref(false)
const filteredItems = computed(() => {
return filterOdd.value
? items.value.filter(item => item.value % 2 === 1)
: items.value
})
const filterItems = () => {
filterOdd.value = true
}
const resetFilter = () => {
filterOdd.value = false
}
const positions = new Map()
const beforeEnter = (el) => {
el.style.opacity = '0'
el.style.transform = 'scale(0.8)'
}
const enter = (el, done) => {
// 获取最终位置
const end = el.getBoundingClientRect()
const start = positions.get(el)
if (start) {
// 计算位置差
const dx = start.left - end.left
const dy = start.top - end.top
const ds = start.width / end.width
// 反向变换
el.style.transform = `translate(${dx}px, ${dy}px) scale(${ds})`
// 强制重绘
el.offsetHeight
// 执行动画
el.style.transition = 'all 0.3s ease'
el.style.transform = ''
el.style.opacity = '1'
setTimeout(done, 300)
} else {
el.style.transition = 'all 0.3s ease'
el.style.transform = ''
el.style.opacity = '1'
setTimeout(done, 300)
}
}
const leave = (el, done) => {
// 记录初始位置
positions.set(el, el.getBoundingClientRect())
el.style.position = 'absolute'
done()
}
</script>
<style>
.flip-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 10px;
position: relative;
min-height: 200px;
}
.flip-item {
background-color: #3498db;
color: white;
padding: 20px;
text-align: center;
border-radius: 8px;
font-weight: bold;
}
.flip-list-enter-active,
.flip-list-leave-active {
transition: all 0.3s ease;
}
.flip-list-enter-from,
.flip-list-leave-to {
opacity: 0;
transform: translateY(30px);
}
.flip-list-move {
transition: transform 0.3s ease;
}
</style>
2. 交错动画
vue
<template>
<div class="stagger-demo">
<button @click="loadItems">加载项目</button>
<button @click="clearItems">清空</button>
<transition-group
name="staggered-fade"
tag="ul"
class="staggered-list"
>
<li
v-for="(item, index) in items"
:key="item.id"
:data-index="index"
class="staggered-item"
>
{{ item.text }}
</li>
</transition-group>
</div>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([])
const loadItems = () => {
items.value = Array.from({ length: 10 }, (_, i) => ({
id: Date.now() + i,
text: `项目 ${i + 1}`
}))
}
const clearItems = () => {
items.value = []
}
</script>
<style>
.staggered-list {
list-style: none;
padding: 0;
}
.staggered-item {
padding: 15px;
margin: 5px 0;
background-color: #e74c3c;
color: white;
border-radius: 6px;
opacity: 0;
}
/* 进入动画 */
.staggered-fade-enter-active {
transition: all 0.3s ease;
}
.staggered-fade-enter-from {
opacity: 0;
transform: translateX(-30px);
}
/* 离开动画 */
.staggered-fade-leave-active {
transition: all 0.3s ease;
position: absolute;
}
.staggered-fade-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* 移动动画 */
.staggered-fade-move {
transition: transform 0.3s ease;
}
/* 交错延迟 */
.staggered-item:nth-child(1) { transition-delay: 0.05s; }
.staggered-item:nth-child(2) { transition-delay: 0.1s; }
.staggered-item:nth-child(3) { transition-delay: 0.15s; }
.staggered-item:nth-child(4) { transition-delay: 0.2s; }
.staggered-item:nth-child(5) { transition-delay: 0.25s; }
.staggered-item:nth-child(6) { transition-delay: 0.3s; }
.staggered-item:nth-child(7) { transition-delay: 0.35s; }
.staggered-item:nth-child(8) { transition-delay: 0.4s; }
.staggered-item:nth-child(9) { transition-delay: 0.45s; }
.staggered-item:nth-child(10) { transition-delay: 0.5s; }
</style>
3. 页面切换动画
vue
<!-- App.vue -->
<template>
<div id="app">
<nav>
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
<router-link to="/contact">联系</router-link>
</nav>
<router-view v-slot="{ Component }">
<transition name="page" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</template>
<style>
.page-enter-active,
.page-leave-active {
transition: all 0.3s ease;
position: absolute;
top: 60px;
left: 0;
right: 0;
}
.page-enter-from {
opacity: 0;
transform: translateX(30px);
}
.page-leave-to {
opacity: 0;
transform: translateX(-30px);
}
nav {
padding: 20px;
background-color: #f8f9fa;
}
nav a {
margin-right: 20px;
text-decoration: none;
color: #333;
}
nav a.router-link-active {
color: #42b883;
font-weight: bold;
}
</style>
性能优化建议
1. 使用 transform 和 opacity
优先使用 transform 和 opacity 属性,因为它们不会触发重排:
css
/* 推荐 */
.good-animation {
transition: transform 0.3s ease, opacity 0.3s ease;
}
/* 避免 */
.bad-animation {
transition: left 0.3s ease, top 0.3s ease;
}
2. 合理使用 will-change
对于复杂的动画,可以提前告知浏览器优化:
css
.animated-element {
will-change: transform, opacity;
}
3. 避免阻塞主线程
对于复杂动画,考虑使用 Web Workers 或 requestAnimationFrame:
javascript
const animateElement = (element, duration) => {
const startTime = performance.now()
const animate = (currentTime) => {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
// 更新元素样式
element.style.transform = `translateX(${progress * 100}px)`
if (progress < 1) {
requestAnimationFrame(animate)
}
}
requestAnimationFrame(animate)
}
结语
Vue 3 的过渡和动画系统为我们提供了强大而灵活的工具来创建丰富的用户界面体验。通过合理运用 <transition> 和 <transition-group> 组件,结合 CSS3 动画和 JavaScript 控制,我们能够实现从简单到复杂的各种动画效果。
关键要点总结:
- 理解过渡类名机制:掌握6个核心类名的作用时机
- 善用 JavaScript 钩子:实现更复杂的自定义动画逻辑
- 列表动画的重要性 :使用
<transition-group>处理动态列表 - 性能优化意识:选择合适的 CSS 属性和动画技术
- 用户体验考量:动画应该增强而不是阻碍用户操作
在实际项目中,建议根据具体需求选择合适的动画方案,并始终考虑性能影响。适度的动画能够显著提升用户体验,但过度或不当的动画反而会适得其反。希望本文能够帮助你在 Vue 3 项目中更好地实现和控制动画效果。