Appearance
【Vue】TransitionGroup
要点速览
- 作用:为“多个元素/列表项”的新增、删除与重新排序应用过渡/移动动画。
- 容器:默认不渲染容器;通过
tag指定容器元素(如ul/div)。 - key:列表项必须具备唯一且稳定的
:key,否则无法正确动画与复用。 - 模式:不支持
mode;因为不是两个互斥元素的切换场景。 - 类名:过渡类应用在子元素上;重新排序使用
name-move类。 - 建议:为
*-move提供transition: transform/opacity等以触发移动动画。
动机与定义
TransitionGroup 是 Vue 的内置组件,专为“多个元素”或“列表项”的过渡而设计。与只包裹单个根节点的 Transition 不同,TransitionGroup 能够在元素新增、删除与重新排序时分别为每一个子元素挂载/移除过渡类。
下面代码把 Transition 包在 ul 外层,新增/删除的是 li,因此不会触发过渡:
vue
<template>
<div class="container">
<div class="btns">
<button @click="addItem">添加项目</button>
<button @click="removeItem">移除项目</button>
</div>
<Transition name="fade">
<ul>
<li v-for="item in items" :key="item" class="box">{{ item }}</li>
</ul>
</Transition>
</div>
</template>
<script setup>
import { ref } from "vue";
const items = ref(["内容1", "内容2", "内容3"]);
const addItem = () => items.value.push(`内容${items.value.length + 1}`);
const removeItem = () => items.value.pop();
</script>
<style scoped>
.container {
text-align: center;
}
.btns button {
margin: 1em 0.5em;
}
.box {
background: #42b983;
color: #fff;
margin: 5px auto;
padding: 10px;
width: 200px;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 1s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>正确做法是使用 TransitionGroup,将过渡直接应用到每个 li 上:
vue
<TransitionGroup name="fade" tag="ul">
<li v-for="item in items" :key="item" class="box">{{ item }}</li>
</TransitionGroup>快速上手
vue
<template>
<div class="container">
<div class="btns">
<button @click="addItem">添加</button>
<button @click="removeItem">移除</button>
</div>
<TransitionGroup name="fade" tag="ul">
<li v-for="item in items" :key="item" class="box">{{ item }}</li>
</TransitionGroup>
</div>
</template>
<script setup>
import { ref } from "vue";
const items = ref(["A", "B", "C"]);
const addItem = () =>
items.value.unshift(`N${Math.random().toString(36).slice(2, 5)}`);
const removeItem = () => items.value.shift();
</script>
<style scoped>
.container {
text-align: center;
}
.btns button {
margin: 1em 0.5em;
}
.box {
background: #42b983;
color: #fff;
margin: 5px auto;
padding: 10px;
width: 200px;
}
.fade-enter-active,
.fade-leave-active,
.fade-move {
transition: all 0.5s;
}
.fade-enter-from {
opacity: 0;
transform: translateX(-100%);
}
.fade-leave-to {
opacity: 0;
transform: translateX(100%);
}
</style>相关细节
TransitionGroup 与 Transition 的共同点:支持相同命名规则的过渡类与 JS 钩子;不同点如下:
- 默认不渲染容器元素;需要容器时使用
tag指定(如ul)。 - 不支持
mode;列表不是两个互斥元素的切换。 - 必须为每个列表项提供唯一且稳定的
:key(避免使用数组下标)。 - 过渡类应用在子元素,而非容器元素;重新排序时为受影响的元素添加
name-move。
实战案例
使用过渡效果优化待办事项的显示与排序动画:
vue
<template>
<div class="container">
<input
type="text"
placeholder="Add a new todo"
class="todo-content"
v-model="newTodo"
@keypress.enter="addTodo"
/>
<TransitionGroup tag="ul" name="fade" class="todo-container">
<li v-for="todo in todos" :key="todo.id" class="todo">
<span>{{ todo.content }}</span>
<button @click="deleteTodo(todo.id)">删除</button>
</li>
</TransitionGroup>
</div>
</template>
<script setup>
import { ref } from "vue";
const newTodo = ref("");
const todos = ref([
{ id: id(), content: "Learn Vue.js", done: false },
{ id: id(), content: "Learn Vite", done: false },
]);
function id() {
return Math.random().toString(36).slice(2, 9);
}
function deleteTodo(id) {
todos.value = todos.value.filter((t) => t.id !== id);
}
function addTodo() {
if (newTodo.value.trim()) {
todos.value.unshift({ id: id(), content: newTodo.value, done: false });
newTodo.value = "";
}
}
</script>
<style scoped>
.container {
width: 600px;
margin: 1em auto;
padding: 1.5em;
border-radius: 5px;
}
.todo-content {
box-sizing: border-box;
width: 100%;
height: 50px;
border-radius: 5px;
outline: none;
font-size: 1.3em;
padding: 0 1em;
border: 1px solid #ccc;
}
.todo-container {
list-style: none;
padding: 0;
margin: 1em 0;
}
.todo {
padding: 0.5em 0;
border-bottom: 1px solid #ccc;
display: flex;
justify-content: space-between;
margin-bottom: 1em;
}
.fade-enter-active,
.fade-leave-active,
.fade-move {
transition: all 0.5s;
}
.fade-enter-from {
opacity: 0;
transform: translateX(-100%);
}
.fade-leave-to {
opacity: 0;
transform: translateX(100%);
}
</style>常见误区与实践建议
- 使用稳定
:key:避免数组下标;使用业务 ID 保持实例稳定与动画可预期。 - 触发移动动画:
name-move只有在存在位移时才会触发;通常通过transform更可靠。 - 类应用位置:所有过渡类加在“子元素”上,不会加在容器(
tag)元素上。 - 模式不可用:不要尝试在
TransitionGroup上使用mode;针对互斥元素切换请用Transition。
