Transition 与动画
Vue3过渡与动画系统
1. Transition 组件
<template>
<Transition name="fade">
<div v-if="show">内容</div>
</Transition>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
2. 过渡类名
| 类名 | 说明 |
|---|---|
v-enter-from | 进入起始状态 |
v-enter-active | 进入生效状态 |
v-enter-to | 进入结束状态 |
v-leave-from | 离开起始状态 |
v-leave-active | 离开生效状态 |
v-leave-to | 离开结束状态 |
3. JavaScript 钩子
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@before-leave="onBeforeLeave"
@leave="onLeave"
@after-leave="onAfterLeave"
>
<div v-if="show">内容</div>
</Transition>
4. TransitionGroup
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">{{ item.text }}</li>
</TransitionGroup>
<style>
.list-move,
.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;
}
</style>
5. 自定义过渡
<Transition
:duration="{ enter: 500, leave: 300 }"
enter-active-class="animate__animated animate__fadeIn"
leave-active-class="animate__animated animate__fadeOut"
>
<div v-if="show">内容</div>
</Transition>
Transition 单元素过渡
Transition 基础用法
<template>
<button @click="show = !show">切换</button>
<Transition name="fade">
<p v-if="show">Hello</p>
</Transition>
</template>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
</style>
<script setup>
import { ref } from 'vue';
const show = ref(true);
</script>
Transition 类名约定
.<name>-enter-from { /* 进入起点 */ }
.<name>-enter-active { /* 进入过程 */ }
.<name>-enter-to { /* 进入终点 */ }
.<name>-leave-from { /* 离开起点 */ }
.<name>-leave-active { /* 离开过程 */ }
.<name>-leave-to { /* 离开终点 */ }
Transition 自定义类名
<Transition
enter-from-class="custom-enter-from"
enter-active-class="custom-enter-active"
enter-to-class="custom-enter-to"
leave-from-class="custom-leave-from"
leave-active-class="custom-leave-active"
leave-to-class="custom-leave-to"
>
<div v-if="show">content</div>
</Transition>
Transition Props
name 与 appear
<Transition name="<name>" appear>
<Transition name="fade" appear>
<p v-if="show">初次渲染也会执行动画</p>
</Transition>
type 与 duration
<Transition type="<transition|animation>" :duration="<ms>">
<Transition type="transition" :duration="1000">
<div v-if="show">content</div>
</Transition>
<Transition :duration="{ enter: 500, leave: 800 }">
<div v-if="show">content</div>
</Transition>
mode 过渡模式
<Transition mode="<out-in|in-out>">
<!-- 先离开再进入 -->
<Transition mode="out-in">
<component :is="currentComp" />
</Transition>
<!-- 同时进行(默认) -->
<Transition>
<div :key="current">content</div>
</Transition>
Transition 钩子
JavaScript 钩子
<Transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
>
<div v-if="show">content</div>
</Transition>
<script setup>
function beforeEnter(el) {
el.style.opacity = 0;
}
function enter(el, done) {
// 调用 done 表示动画完成
el.offsetHeight; // 触发重排
el.style.transition = 'opacity 0.5s';
el.style.opacity = 1;
el.addEventListener('transitionend', done);
}
function afterEnter(el) {
console.log('enter 完成');
}
function leave(el, done) {
el.style.opacity = 0;
el.addEventListener('transitionend', done);
}
</script>
CSS 与 JS 钩子组合
<Transition
name="fade"
@enter="onEnter"
:css="false"
>
<div v-if="show">content</div>
</Transition>
<script setup>
function onEnter(el, done) {
// :css="false" 跳过 CSS 类名规则,完全用 JS 控制
gsap.to(el, { opacity: 1, duration: 0.5, onComplete: done });
}
</script>
TransitionGroup 列表过渡
TransitionGroup 基础
<template>
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</TransitionGroup>
</template>
<style>
.list-enter-active, .list-leave-active {
transition: all 0.5s;
}
.list-enter-from, .list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-move {
transition: transform 0.5s;
}
</style>
TransitionGroup Props
<TransitionGroup
name="list"
tag="ul"
appear
:css="true"
>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</TransitionGroup>
移动动画
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</TransitionGroup>
<style>
.list-move {
transition: transform 0.5s ease;
}
.list-enter-active, .list-leave-active {
transition: all 0.5s ease;
}
.list-leave-active {
position: absolute; /* 离开时脱离文档流,触发 move 动画 */
}
</style>
TransitionGroup 钩子
<TransitionGroup
tag="ul"
@before-enter="onBeforeEnter"
@enter="onEnter"
@leave="onLeave"
@before-leave="onBeforeLeave"
>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</TransitionGroup>
动画集成
CSS animation 动画
<Transition name="bounce">
<p v-if="show">Bounce!</p>
</Transition>
<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); }
50% { transform: scale(1.25); }
100% { transform: scale(1); }
}
</style>
GSAP 集成
<script setup>
import { ref } from 'vue';
import gsap from 'gsap';
const show = ref(true);
function onEnter(el, done) {
gsap.fromTo(el,
{ opacity: 0, y: 30 },
{ opacity: 1, y: 0, duration: 0.5, onComplete: done }
);
}
function onLeave(el, done) {
gsap.to(el, {
opacity: 0, y: -30, duration: 0.5, onComplete: done
});
}
</script>
<template>
<Transition :css="false" @enter="onEnter" @leave="onLeave">
<p v-if="show">Animated</p>
</Transition>
</template>
综合应用
列表删除/添加/排序动画
<script setup>
import { ref, reactive } from 'vue';
const items = ref([
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
{ id: 3, name: 'C' }
]);
let nextId = 4;
function add() {
items.value.push({ id: nextId++, name: String.fromCharCode(64 + nextId) });
}
function remove(index) {
items.value.splice(index, 1);
}
function shuffle() {
items.value = items.value.sort(() => Math.random() - 0.5);
}
</script>
<template>
<button @click="add">添加</button>
<button @click="shuffle">随机排序</button>
<TransitionGroup name="list" tag="ul">
<li v-for="(item, index) in items" :key="item.id">
{{ item.name }}
<button @click="remove(index)">删除</button>
</li>
</TransitionGroup>
</template>
<style>
.list-enter-active, .list-leave-active, .list-move {
transition: all 0.5s ease;
}
.list-enter-from, .list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-leave-active {
position: absolute;
}
</style>
Transition + 动态组件
<template>
<button @click="toggle">切换</button>
<Transition name="fade" mode="out-in">
<component :is="currentComp" />
</Transition>
</template>
<script setup>
import { ref, computed, shallowRef } from 'vue';
import CompA from './CompA.vue';
import CompB from './CompB.vue';
const isA = ref(true);
const currentComp = computed(() => isA.value ? CompA : CompB);
const toggle = () => { isA.value = !isA.value; };
</script>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
</style>
常见陷阱
| 陷阱 | 原因 | 正确做法 |
|---|---|---|
<Transition> 包裹多个元素 | Transition 只支持单一元素或组件 | 只放一个根节点;列表动画改用 <TransitionGroup> |
| 切换相同标签不触发动画 | Vue 复用了同标签元素,没有替换发生 | 给元素加不同的 key(<div :key="state">) |
| CSS animation 写了却不生效 | Transition 默认监听 transitionend,动画用的是 animation | 加 type="animation",或显式 :duration |
JS 钩子只写 @enter 不调用 done | 没有完成信号,Vue 立即结束过渡 | 用 :css="false" 并在动画结束时调用 done() |
TransitionGroup 忘了 position: absolute | 离开项仍占位,move 动画错乱 | .list-leave-active { position: absolute } 让离开项脱离文档流 |
模式写反:mode="in-out" 用于路由切换 | 新页面先进入、旧页面后离开,出现叠影 | 路由/互斥内容切换用 mode="out-in" |