Vue 动画深入
约 1211 字大约 4 分钟
vueanimation
2026-04-16
Vue 动画概述
Vue 提供了强大的动画能力,主要用于:
- 条件渲染(v-if)
- 条件展示(v-show)
- 动态组件切换
- 列表渲染(transition-group)
Vue 的动画系统基于 CSS 动画和 JavaScript 动画钩子,可以优雅地处理元素的进入和离开效果。
transition 组件
<transition> 是 Vue 提供的内置组件,用于包装需要动画的元素。
条件渲染(v-if)
<template>
<button @click="show = !show">切换</button>
<transition>
<div v-if="show" class="box">内容</div>
</transition>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(true)
</script>
<style scoped>
.box {
width: 100px;
height: 100px;
background: #42b983;
}
/* 进入动画 */
.v-enter-active,
.v-leave-active {
transition: all 0.3s ease;
}
/* 进入开始状态 / 离开结束状态 */
.v-enter-from,
.v-leave-to {
opacity: 0;
transform: translateX(20px);
}
</style>条件展示(v-show)
v-show 与 v-if 的动画机制类似,但 v-show 不会销毁元素:
<transition>
<div v-show="visible" class="modal">弹窗内容</div>
</transition>动态组件
<template>
<button @click="current = current === 'A' ? 'B' : 'A'">切换</button>
<transition name="fade" mode="out-in">
<component :is="current === 'A' ? ComponentA : ComponentB" />
</transition>
</template>列表(transition-group)
详见下文「列表动画」章节。
CSS 动画
name / duration / mode
<transition
name="my-transition"
:duration="{ enter: 500, leave: 300 }"
mode="out-in"
>
<div v-if="show">内容</div>
</transition>对应 CSS 类名:
.my-transition-enter-active { /* 进入动画 */ }
.my-transition-leave-active { /* 离开动画 */ }
.my-transition-enter-from { /* 进入初始状态 */ }
.my-transition-leave-to { /* 离开结束状态 */ }
.my-transition-move { /* 移动动画 */ }自定义类名
可以通过以下属性自定义类名:
<transition
enter-active-class="animate__animated animate__bounce"
leave-active-class="animate__animated animate__fadeOut"
>
<div v-if="show">内容</div>
</transition>动画库集成(Animate.css)
<template>
<transition
enter-active-class="animate__animated animate__bounceIn"
leave-active-class="animate__animated animate__bounceOut"
>
<div v-if="show" class="box">Animate.css 动画</div>
</transition>
</template>
<style>
@import 'animate.css';
.box {
width: 100px;
height: 100px;
background: #42b983;
}
</style>JavaScript 动画钩子
Vue 提供了 6 个动画钩子事件:
<transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@before-leave="onBeforeLeave"
@leave="onLeave"
@after-leave="onAfterLeave"
>
<div v-if="show" class="box">内容</div>
</transition>const onBeforeEnter = (el) => {
el.style.opacity = 0
el.style.transform = 'translateX(-20px)'
}
const onEnter = (el, done) => {
// 动画完成后调用 done
Velocity(el, {
opacity: 1,
translateX: 0
}, {
duration: 300,
complete: done
})
}
const onAfterEnter = (el) => {
console.log('动画完成')
}
const onBeforeLeave = (el) => {
el.style.opacity = 1
}
const onLeave = (el, done) => {
Velocity(el, {
opacity: 0,
translateX: 20
}, {
duration: 300,
complete: done
})
}
const onAfterLeave = (el) => {
console.log('离开动画完成')
}结合 Velocity
import Velocity from 'velocity-animate'
const animateEnter = (el, done) => {
Velocity(el, 'slideDown', {
complete: done
})
}
const animateLeave = (el, done) => {
Velocity(el, 'slideUp', {
complete: done
})
}结合 GSAP
import { gsap } from 'gsap'
const onEnter = (el, done) => {
gsap.fromTo(el,
{ opacity: 0, y: 20 },
{
opacity: 1,
y: 0,
duration: 0.3,
onComplete: done
}
)
}
const onLeave = (el, done) => {
gsap.to(el, {
opacity: 0,
y: -20,
duration: 0.3,
onComplete: done
})
}列表动画(transition-group)
<transition-group> 用于列表的进入、离开和移动动画。
基本用法
<template>
<button @click="addItem">添加</button>
<button @click="removeItem">删除</button>
<transition-group name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</transition-group>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, text: 'item 1' },
{ id: 2, text: 'item 2' },
{ id: 3, text: 'item 3' }
])
const addItem = () => {
items.value.push({
id: Date.now(),
text: `item ${items.value.length + 1}`
})
}
const removeItem = () => {
items.value.pop()
}
</script>
<style scoped>
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-move {
transition: transform 0.3s ease;
}
</style>移动动画
当列表顺序发生变化时,使用 list-move 类实现平滑移动:
.list-move {
transition: transform 0.5s cubic-bezier(0.5, 0, 0.5, 1);
}进入/离开
.list-enter-active {
transition: all 0.5s ease-out;
}
.list-leave-active {
transition: all 0.5s ease-in;
position: absolute;
}
.list-enter-from {
opacity: 0;
transform: translateY(-30px);
}
.list-leave-to {
opacity: 0;
transform: translateY(30px);
}交错动画
通过绑定动画钩子实现列表元素的交错动画:
<template>
<transition-group name="list" tag="ul" @enter="onEnter">
<li v-for="(item, index) in items" :key="item.id" :data-index="index">
{{ item.text }}
</li>
</transition-group>
</template>
<script setup>
import { ref } from 'vue'
import { gsap } from 'gsap'
const items = ref([
{ id: 1, text: 'item 1' },
{ id: 2, text: 'item 2' },
{ id: 3, text: 'item 3' }
])
const onEnter = (el, done) => {
const index = el.dataset.index
gsap.fromTo(el,
{ opacity: 0, y: 20 },
{
opacity: 1,
y: 0,
duration: 0.3,
delay: index * 0.1,
onComplete: done
}
)
}
</script>复用动画组件
renderless component
封装一个可复用的过渡组件:
// TransitionEffect.js
import { defineComponent, ref, watch } from 'vue'
export const TransitionEffect = defineComponent({
props: {
show: Boolean,
enterClass: String,
leaveClass: String
},
setup(props, { slots }) {
return () => {
if (props.show) {
return slots.default?.()
}
return null
}
}
})<!-- FadeTransition.vue -->
<script setup>
import { ref, watch } from 'vue'
import Velocity from 'velocity-animate'
const props = defineProps({
show: Boolean
})
const el = ref(null)
watch(() => props.show, (newVal) => {
if (newVal) {
Velocity(el.value, 'fadeIn', { duration: 300 })
} else {
Velocity(el.value, 'fadeOut', { duration: 300 })
}
})
</script>
<template>
<div ref="el">
<slot />
</div>
</template>composable 设计
// useTransition.js
import { ref, watch } from 'vue'
import { gsap } from 'gsap'
export function useTransition(options = {}) {
const { duration = 0.3 } = options
const animateEnter = (el) => {
gsap.fromTo(el,
{ opacity: 0, scale: 0.9 },
{ opacity: 1, scale: 1, duration }
)
}
const animateLeave = (el, done) => {
gsap.to(el, {
opacity: 0,
scale: 0.9,
duration,
onComplete: done
})
}
return {
onBeforeEnter: (el) => { el.style.opacity = 0 },
onEnter: (el, done) => { animateEnter(el); done() },
onAfterEnter: (el) => { el.style.opacity = '' },
onBeforeLeave: (el) => { el.style.opacity = 1 },
onLeave: (el, done) => { animateLeave(el, done) },
onAfterLeave: (el) => { el.style.opacity = '' }
}
}<template>
<transition
:css="false"
@before-enter="onBeforeEnter"
@enter="onEnter"
@leave="onLeave"
>
<div v-if="show" class="box">内容</div>
</transition>
</template>
<script setup>
import { ref } from 'vue'
import { useTransition } from './useTransition'
const show = ref(true)
const { onBeforeEnter, onEnter, onLeave } = useTransition({ duration: 0.5 })
</script>