讲解
界面元素的出现和消失,如果瞬间切换会很生硬。Vue 的 <Transition> 组件把「进入」和「离开」变成有过程的动画:把需要过渡的元素包起来,Vue 会在合适的时机自动添加/移除一组 CSS class,你只需要写 CSS 过渡规则。核心 class 六个:v-enter-from / v-enter-active / v-enter-to(进入的起点、过程、终点)和 v-leave-from / v-leave-active / v-leave-to(离开三段),配合 transition 或 animation 属性就能工作。
实际使用记住三个要点。第一,用 name 属性定制 class 前缀:<Transition name="fade"> 对应 .fade-enter-from 等,避免多个过渡互相干扰。第二,离开过渡期间元素仍在 DOM 中(只是加了 class),所以 v-if 切换的元素可以放心包 Transition。第三,mode="out-in" 解决「新旧元素同时占位」的跳动问题——先让旧的离开完,再让新的进入。
列表的增删改用 <TransitionGroup>:它渲染真实容器元素(tag="ul"),对子项的进入/离开/移动都做过渡,移动动画基于 FLIP 技术自动计算位移。配合 v-for 使用,子项必须有稳定 key。
什么时候不用 Transition?需要 JS 驱动的高自由度动画(拖拽轨迹、弹簧物理、滚动驱动)用 requestAnimationFrame 或动画库(GSAP 等),Transition 管的是「状态切换时的入场/出场」这一类声明式动画。
示例
把过渡的 class 状态机用 JS 精确模拟:一个元素从「进入」到「离开」经过哪些状态(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// Transition 的状态机:记录进入/离开各阶段施加的 class
function transition(name) {
const log = [];
return {
enter() {
log.push(name + '-enter-from'); // 第 1 帧:起点
log.push(name + '-enter-active'); // 第 1 帧:过程(含 transition 定义)
// 下一帧:
log.push('移除 ' + name + '-enter-from');
log.push(name + '-enter-to'); // 终点
// 过渡结束后:
log.push('移除 ' + name + '-enter-active / ' + name + '-enter-to');
},
leave() {
log.push(name + '-leave-from');
log.push(name + '-leave-active');
log.push('移除 ' + name + '-leave-from');
log.push(name + '-leave-to');
log.push('过渡结束,元素从 DOM 移除');
},
log,
};
}
const t = transition('fade');
t.enter();
t.leave();
assert.deepStrictEqual(t.log, [
'fade-enter-from',
'fade-enter-active',
'移除 fade-enter-from',
'fade-enter-to',
'移除 fade-enter-active / fade-enter-to',
'fade-leave-from',
'fade-leave-active',
'移除 fade-leave-from',
'fade-leave-to',
'过渡结束,元素从 DOM 移除',
]);
// TransitionGroup 的移动检测(FLIP 思想:记录旧位置,比对的新位置)
const before = { a: 0, b: 60, c: 120 }; // 每项的 y 坐标
const after = { a: 60, c: 0, d: 120 }; // b 被删、c 上移、d 新增
const moved = Object.keys(after).filter((k) => before[k] !== undefined && before[k] !== after[k]);
const added = Object.keys(after).filter((k) => before[k] === undefined);
assert.deepStrictEqual(moved, ['a', 'c']);
assert.deepStrictEqual(added, ['d']);
console.log('过渡 class 序列共', t.log.length, '步,首尾两帧由浏览器自动过渡');
console.log('列表变化: 移动', moved.join('/'), ',新增', added.join('/'), ',删除 b');
模板与 CSS:
<script setup>
import { ref } from 'vue';
const show = ref(true);
const items = ref([1, 2, 3]);
let next = 4;
</script>
<template>
<button @click="show = !show">切换</button>
<Transition name="fade">
<p v-if="show">我会淡入淡出</p>
</Transition>
<button @click="items.push(next++)">添加</button>
<TransitionGroup tag="ul" name="list">
<li v-for="i in items" :key="i" @click="items = items.filter((x) => x !== i)">
第 {{ i }} 项(点击删除)
</li>
</TransitionGroup>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.list-move,
.list-enter-active,
.list-leave-active {
transition: all 0.3s;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
</style>
常见坑
- 只写 enter 忘了 leave:进入有动画离开秒消失,六个 class 成对理解。
- Transition 包了多个根元素:它只接管单根节点,多根要用 v-if/v-else 保证同时只有一个。
- TransitionGroup 的子项没 key:移动动画依赖 key 识别元素,缺 key 全部按进入/离开处理。
- transition 属性写错 class:持续时间要写在 *-active 上(或 from/to 上用 animation),写在 to 上会出现「结束时才过渡」。
- CSS 没有初始帧概念:enter-from 只存在一帧就被移除,别指望它长期存在;调试时给 active 加很长 duration 观察。
小结
Transition 自动施加进入/离开三段 class,你写 CSS;TransitionGroup 管列表增删移动;复杂物理动画交给 JS 动画库。下一章扩展模板能力:自定义指令。