Vue3 过渡与动画完全指南

小飞兽 Vue.js 157 次阅读 2026-07-25

Vue3 过渡与动画完全指南

概述

Vue3 提供了 <Transition><TransitionGroup> 两个内置组件,用于在元素或组件显隐、状态变化时添加动画效果。配合 CSS transition 和 CSS animation,几乎所有场景的过渡动画都能以声明式方式实现,无需引入额外的动画库。

过渡的核心原理

Vue 的过渡系统会在元素进入或离开 DOM 时自动添加/移除 CSS 类名,开发者只需编写对应的 CSS 过渡样式。<Transition> 用于单个元素/组件的显隐切换;<TransitionGroup> 用于列表中多个元素的添加/移除/移动动画。

基础语法

// Transition:单个元素/组件的显隐过渡
<Transition name="fade">
  <div v-if="show">内容</div>
</Transition>

// TransitionGroup:列表元素的移动/添加/删除动画
<TransitionGroup name="list">
  <div v-for="item in items" :key="item.id">{{ item.name }}</div>
</TransitionGroup>

Transition 触发的 CSS 类名(name="fade"):

  • 进入:<code>fade-enter-from</code> → <code>fade-enter-active</code> → <code>fade-enter-to</code>

  • 离开:<code>fade-leave-from</code> → <code>fade-leave-active</code> → <code>fade-leave-to</code>

完整代码示例

// ========== AnimationsDemo.vue ==========
<script setup>
import { ref, reactive } from 'vue'

// —— 基础显隐动画 ——
const showBasic = ref(true)

// —— 多元素切换动画 ——
const isLogin = ref(true)

// —— 列表动画 ——
const todos = reactive([
  { id: 1, text: '学习 Vue3 Transition', done: false },
  { id: 2, text: '掌握 CSS Animation', done: false },
  { id: 3, text: '完成项目实战', done: false },
])

const newTodo = ref('')

function addTodo() {
  if (!newTodo.value.trim()) return
  todos.push({
    id: Date.now(),
    text: newTodo.value.trim(),
    done: false
  })
  newTodo.value = ''
}

function removeTodo(id) {
  const index = todos.findIndex(t => t.id === id)
  if (index !== -1) todos.splice(index, 1)
}

function toggleDone(id) {
  const todo = todos.find(t => t.id === id)
  if (todo) todo.done = !todo.done
}

// —— 数字滚动动画 ——
const displayNumber = ref(0)
const targetNumber = ref(256)
const animating = ref(false)

function animateNumber() {
  if (animating.value) return
  animating.value = true
  displayNumber.value = 0
  const duration = 1500
  const startTime = Date.now()

  function step() {
    const elapsed = Date.now() - startTime
    const progress = Math.min(elapsed / duration, 1)
    // 缓动函数:easeOutCubic
    const ease = 1 - Math.pow(1 - progress, 3)
    displayNumber.value = Math.round(ease * targetNumber.value)
    if (progress < 1) {
      requestAnimationFrame(step)
    } else {
      animating.value = false
    }
  }
  requestAnimationFrame(step)
}

// —— 模式切换动画(淡入+滑动)——
const currentMode = ref('view')
</script>

<template>
  <div class="animation-demo">

    <!-- ==================== 1. 基础显隐 ==================== -->
    <section>
      <h3>1. 基础显隐动画(Fade)</h3>
      <button @click="showBasic = !showBasic">切换</button>
      <Transition name="fade">
        <div v-if="showBasic" class="box box-primary">淡入淡出</div>
      </Transition>
    </section>

    <!-- ==================== 2. 滑动 + 缩放 ==================== -->
    <section>
      <h3>2. 滑动 + 缩放组合动画</h3>
      <button @click="isLogin = !isLogin">
        {{ isLogin ? '切换到注册' : '切换到登录' }}
      </button>
      <div class="form-container">
        <Transition name="slide-up" mode="out-in">
          <div v-if="isLogin" key="login" class="form-box">
            <h4>登录</h4>
            <input placeholder="用户名" />
            <input placeholder="密码" type="password" />
          </div>
          <div v-else key="register" class="form-box">
            <h4>注册</h4>
            <input placeholder="邮箱" />
            <input placeholder="密码" type="password" />
            <input placeholder="确认密码" type="password" />
          </div>
        </Transition>
      </div>
    </section>

    <!-- ==================== 3. 列表过渡 ==================== -->
    <section>
      <h3>3. 待办事项列表(列表过渡动画)</h3>
      <div class="todo-input">
        <input v-model="newTodo" @keydown.enter="addTodo" placeholder="添加新待办..." />
        <button @click="addTodo">添加</button>
      </div>
      <TransitionGroup name="todo-list" tag="ul" class="todo-list">
        <li
          v-for="todo in todos"
          :key="todo.id"
          :class="{ done: todo.done }"
          @click="toggleDone(todo.id)"
        >
          <span>{{ todo.text }}</span>
          <button @click.stop="removeTodo(todo.id)">&times;</button>
        </li>
      </TransitionGroup>
      <p style="font-size: 0.8rem; color: #888;">
        tip: 点击条目切换完成状态,点击 &times; 删除
      </p>
    </section>

    <!-- ==================== 4. 数字动画 ==================== -->
    <section>
      <h3>4. 数字滚动动画</h3>
      <div class="number-display">{{ displayNumber }}</div>
      <button @click="animateNumber" :disabled="animating">
        {{ animating ? '动画中...' : '触发动画' }}
      </button>
    </section>

  </div>
</template>

<style scoped>
/* ==================== 1. Fade 动画 ==================== */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* ==================== 2. 滑动 + 缩放组合 ==================== */
.slide-up-enter-active {
  transition: all 0.4s ease-out;
}
.slide-up-leave-active {
  transition: all 0.3s ease-in;
}
.slide-up-enter-from {
  opacity: 0;
  transform: translateY(30px) scale(0.95);
}
.slide-up-leave-to {
  opacity: 0;
  transform: translateY(-30px) scale(0.95);
}

/* ==================== 3. 列表过渡动画 ==================== */
.todo-list {
  list-style: none;
  padding: 0;
  position: relative;
}

.todo-list li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.75rem;
  margin: 0.25rem 0;
  background: white;
  border: 1px solid #e0e0e0;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.3s ease;
}

.todo-list li.done span {
  text-decoration: line-through;
  color: #999;
}

.todo-list li button {
  background: #e74c3c;
  color: white;
  border: none;
  border-radius: 50%;
  width: 24px;
  height: 24px;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
}

/* 入场:从下方滑入 + 淡入 */
.todo-list-enter-active {
  transition: all 0.4s ease-out;
}
.todo-list-leave-active {
  transition: all 0.3s ease-in;
  position: absolute;  /* 离开时脱离文档流,为 move 动画让位 */
  width: 100%;
}
/* 入场起点 */
.todo-list-enter-from {
  opacity: 0;
  transform: translateX(-30px);
}
/* 离场终点 */
.todo-list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}
/* 列表移动动画(添加/删除时其余元素平滑移动)*/
.todo-list-move {
  transition: transform 0.4s ease;
}

/* ==================== 通用 ==================== */
section {
  margin: 1.5rem 0;
  padding: 1rem;
  border: 1px solid #eee;
  border-radius: 8px;
}

.box {
  width: 150px;
  height: 80px;
  margin-top: 0.5rem;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: bold;
}

.box-primary { background: #42b983; }

.form-container { margin-top: 0.5rem; }
.form-box {
  padding: 1rem;
  border: 1px solid #42b983;
  border-radius: 8px;
  background: #f8fff8;
}
.form-box h4 { margin: 0 0 0.5rem; }
.form-box input {
  display: block;
  width: 100%;
  padding: 0.5rem;
  margin-bottom: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
}

.todo-input {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}
.todo-input input {
  flex: 1;
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
}
.todo-input button {
  padding: 0.5rem 1rem;
  background: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.number-display {
  font-size: 3rem;
  font-weight: bold;
  color: #42b983;
  font-variant-numeric: tabular-nums;
}
</style>

运行效果说明

基础显隐点击切换时内容平滑淡入淡出。登录/注册表单切换时使用 mode="out-in" 确保一个离开完成后再入场新的,配合 translateY 实现上下滑动。列表添加/删除时,<TransitionGroup> 自动计算位置变化并添加 move 动画,其余条目平滑过渡。数字滚动点击后数字从 0 平滑增长到 256,采用 easeOutCubic 缓动曲线使动画自然减速。

常见问题

Q1:<code>&lt;Transition&gt;</code> 的 <code>mode</code> 属性有什么作用?

mode 可选值:"in-out"(新元素先完成入场,再移除旧元素)和 "out-in"(旧元素先完成离场,再渲染新元素)。不设置 mode 时,新旧元素同时执行过渡,适合非互斥的显隐场景。登录/注册切换适合 "out-in",避免两个表单同时存在于 DOM 中造成布局混乱。

Q2:<code>&lt;TransitionGroup&gt;</code> 中删除元素时其余元素抖动/不流畅怎么办?

常见原因:删除元素的容器没有设置 position: relative。由于 move 动画依赖 Transform 实现平滑移动,容器必须有明确的高度约束。另一个常见问题:列表的 :key 使用了数组索引 index,导致 Vue 无法正确追踪元素身份,应始终使用唯一 ID 作为 :key

Q3:Vue 过渡动画和第三方动画库(如 GSAP)如何结合?

将 GSAP 的 gsap.from() / gsap.to() 与 Vue 的 JS 过渡钩子结合使用。在 <Transition>@enter / @leave 钩子中调用 GSAP 方法,完全控制动画细节,实现 Vue 声明式模板 + GSAP 高性能动画的组合。GSAP 还支持 FLIP(First-Last-Invert-Play)技术处理复杂列表重排动画。

延伸阅读

  • <a href="#">Vue3 组合式 API 完全指南</a> —— 组合式 API 与动画组件的结合
  • <a href="#">Vue3 生命周期钩子详解</a> —— 在钩子中集成第三方动画初始化逻辑
  • <a href="#">Vue3 自定义指令完全指南</a> —— 用自定义指令实现复杂 DOM 动画场景