Vue3 动画与过渡

Vue3 的 Transition 和 TransitionGroup 组件让界面交互更加生动,从 CSS 类名变化到 JavaScript 钩子,全面掌握动画系统的每一个细节。

一、前言

动画不仅是视觉装饰,更是提升用户体验的重要手段。良好的过渡动画可以:

  • 帮助用户理解界面状态的变化
  • 缓解等待焦虑(加载动画)
  • 引导用户注意力
  • 增强界面的专业感和精致度

Vue3 继承了 Vue2 的过渡系统,并进行了优化和调整。本文将系统讲解 Vue3 中的动画与过渡,包括基础用法、JavaScript 钩子、列表动画以及与第三方库的集成。

二、Transition 组件

2.1 基本用法

<Transition> 是 Vue3 内置的过渡组件,包裹元素或组件后,在插入、更新或移除 DOM 时自动应用过渡类名。

<template>
  <button @click="show = !show">切换显示</button>
  
  <Transition>
    <p v-if="show" class="greeting">你好,Vue3!</p>
  </Transition>
</template>

<script setup>
import { ref } from 'vue'

const show = ref(true)
</script>

<style scoped>
/* 进入动画 */
.v-enter-active {
  transition: opacity 0.5s ease;
}

.v-enter-from {
  opacity: 0;
}

.v-enter-to {
  opacity: 1;
}

/* 离开动画 */
.v-leave-active {
  transition: opacity 0.5s ease;
}

.v-leave-from {
  opacity: 1;
}

.v-leave-to {
  opacity: 0;
}
</style>

2.2 Vue3 过渡类名变化

Vue3 对过渡类名进行了调整,这是与 Vue2 最大的区别之一:

状态 Vue2 类名 Vue3 类名 说明
进入开始 v-enter v-enter-from 元素插入前的初始状态
进入中 v-enter-active v-enter-active 整个进入过渡阶段
进入结束 v-enter-to v-enter-to 元素插入后的最终状态
离开开始 v-leave v-leave-from 元素移除前的初始状态
离开中 v-leave-active v-leave-active 整个离开过渡阶段
离开结束 v-leave-to v-leave-to 元素移除后的最终状态

关键变化:Vue3 将 v-enter 重命名为 v-enter-from,将 v-leave 重命名为 v-leave-from,使命名更加语义化。

2.3 自定义过渡类名

当需要集成第三方 CSS 动画库(如 Animate.css)时,可以使用自定义类名:

<template>
  <Transition
    enter-active-class="animate__animated animate__bounceIn"
    leave-active-class="animate__animated animate__bounceOut"
  >
    <p v-if="show" class="animate__animated">Animate.css 动画</p>
  </Transition>
</template>

<script setup>
import { ref } from 'vue'

const show = ref(true)
</script>

<style>
/* 引入 Animate.css */
@import 'animate.css';
</style>

2.4 命名过渡

通过 name 属性可以自定义过渡类名的前缀:

<template>
  <Transition name="fade">
    <p v-if="show">命名过渡</p>
  </Transition>
</template>

<script setup>
import { ref } from 'vue'

const show = ref(true)
</script>

<style scoped>
/* 使用 fade- 前缀 */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

2.5 CSS 过渡 vs CSS 动画

Vue 的过渡系统同时支持 CSS 过渡(transition)和 CSS 动画(animation):

<style scoped>
/* CSS 过渡方式 */
.slide-enter-active {
  transition: transform 0.3s ease;
}

.slide-enter-from {
  transform: translateX(-100%);
}

/* CSS 动画方式 */
.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>

三、TransitionGroup 组件

3.1 列表过渡基础

<TransitionGroup> 用于对 v-for 列表进行过渡动画:

<template>
  <div>
    <button @click="addItem">添加项目</button>
    <button @click="shuffle">打乱顺序</button>
    
    <TransitionGroup name="list" tag="ul">
      <li v-for="item in items" :key="item.id">
        {{ item.text }}
        <button @click="removeItem(item.id)">删除</button>
      </li>
    </TransitionGroup>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const items = ref([
  { id: 1, text: '项目 1' },
  { id: 2, text: '项目 2' },
  { id: 3, text: '项目 3' }
])

let nextId = 4

const addItem = () => {
  items.value.push({ id: nextId++, text: `项目 ${nextId - 1}` })
}

const removeItem = (id) => {
  const index = items.value.findIndex(item => item.id === id)
  if (index > -1) {
    items.value.splice(index, 1)
  }
}

const shuffle = () => {
  items.value = items.value.sort(() => Math.random() - 0.5)
}
</script>

<style scoped>
.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;
}

/* 列表项移动时的过渡 */
.list-move {
  transition: transform 0.5s ease;
}
</style>

3.2 FLIP 动画原理

TransitionGroup 内部使用了 FLIP 动画技术:

  • First:记录元素的初始位置
  • Last:记录元素的最终位置
  • Invert:计算并应用反向偏移
  • Play:播放过渡动画

记录 First

记录 Last

Invert

Play

元素初始位置

计算最终位置

应用反向偏移

触发过渡动画

元素平滑移动到新位置

TransitionGroup 会自动处理这个过程,开发者只需定义 .v-move 类即可。

3.3 列表排序动画

<template>
  <div class="todo-app">
    <div class="filters">
      <button @click="sortBy = 'default'">默认</button>
      <button @click="sortBy = 'priority'">按优先级</button>
      <button @click="sortBy = 'date'">按日期</button>
    </div>
    
    <TransitionGroup name="todo" tag="div" class="todo-list">
      <div
        v-for="todo in sortedTodos"
        :key="todo.id"
        class="todo-item"
        :class="`priority-${todo.priority}`"
      >
        <span>{{ todo.title }}</span>
        <span>{{ todo.date }}</span>
      </div>
    </TransitionGroup>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const sortBy = ref('default')

const todos = ref([
  { id: 1, title: '完成项目文档', priority: 'high', date: '2024-01-15' },
  { id: 2, title: '修复 Bug', priority: 'medium', date: '2024-01-10' },
  { id: 3, title: '代码审查', priority: 'low', date: '2024-01-20' },
  { id: 4, title: '部署上线', priority: 'high', date: '2024-01-12' }
])

const sortedTodos = computed(() => {
  const list = [...todos.value]
  switch (sortBy.value) {
    case 'priority':
      const priorityMap = { high: 3, medium: 2, low: 1 }
      return list.sort((a, b) => priorityMap[b.priority] - priorityMap[a.priority])
    case 'date':
      return list.sort((a, b) => new Date(a.date) - new Date(b.date))
    default:
      return list
  }
})
</script>

<style scoped>
.todo-list {
  position: relative;
}

.todo-item {
  padding: 12px;
  margin: 8px 0;
  border-radius: 6px;
  background: #f5f5f5;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.priority-high { border-left: 4px solid #f44336; }
.priority-medium { border-left: 4px solid #ff9800; }
.priority-low { border-left: 4px solid #4caf50; }

.todo-enter-active,
.todo-leave-active {
  transition: all 0.4s ease;
}

.todo-enter-from,
.todo-leave-to {
  opacity: 0;
  transform: scale(0.9);
}

.todo-leave-active {
  position: absolute;
  width: 100%;
}

.todo-move {
  transition: transform 0.4s ease;
}
</style>

四、JavaScript 钩子

4.1 完整的钩子函数

当 CSS 动画无法满足需求时,可以使用 JavaScript 钩子进行精细控制:

<template>
  <Transition
    @before-enter="beforeEnter"
    @enter="enter"
    @after-enter="afterEnter"
    @enter-cancelled="enterCancelled"
    @before-leave="beforeLeave"
    @leave="leave"
    @after-leave="afterLeave"
    @leave-cancelled="leaveCancelled"
    :css="false"
  >
    <div v-if="show" class="js-animated-box">
      JavaScript 动画控制
    </div>
  </Transition>
</template>

<script setup>
import { ref } from 'vue'

const show = ref(true)

// 进入前
const beforeEnter = (el) => {
  el.style.opacity = '0'
  el.style.transform = 'scale(0)'
}

// 进入中
const enter = (el, done) => {
  // 使用 Web Animations API
  const animation = el.animate([
    { opacity: 0, transform: 'scale(0)' },
    { opacity: 1, transform: 'scale(1)' }
  ], {
    duration: 500,
    easing: 'ease-out'
  })
  
  animation.onfinish = () => done()
}

// 进入完成
const afterEnter = (el) => {
  console.log('进入动画完成')
}

// 进入取消
const enterCancelled = (el) => {
  console.log('进入动画被取消')
}

// 离开前
const beforeLeave = (el) => {
  el.style.opacity = '1'
}

// 离开中
const leave = (el, done) => {
  const animation = el.animate([
    { opacity: 1, transform: 'scale(1)' },
    { opacity: 0, transform: 'scale(0)' }
  ], {
    duration: 300,
    easing: 'ease-in'
  })
  
  animation.onfinish = () => done()
}

// 离开完成
const afterLeave = (el) => {
  console.log('离开动画完成')
}

// 离开取消
const leaveCancelled = (el) => {
  console.log('离开动画被取消')
}
</script>

4.2 与 GSAP 集成

GSAP 是一个强大的 JavaScript 动画库,与 Vue 的过渡系统配合使用效果极佳:

<template>
  <Transition
    @enter="onEnter"
    @leave="onLeave"
    :css="false"
    appear
  >
    <div v-if="show" class="gsap-box">
      GSAP 动画
    </div>
  </Transition>
</template>

<script setup>
import { ref } from 'vue'
import gsap from 'gsap'

const show = ref(true)

const onEnter = (el, done) => {
  gsap.fromTo(el,
    { opacity: 0, y: 50, scale: 0.8 },
    {
      opacity: 1,
      y: 0,
      scale: 1,
      duration: 0.6,
      ease: 'back.out(1.7)',
      onComplete: done
    }
  )
}

const onLeave = (el, done) => {
  gsap.to(el, {
    opacity: 0,
    y: -50,
    scale: 0.8,
    duration: 0.4,
    ease: 'power2.in',
    onComplete: done
  })
}
</script>

<style scoped>
.gsap-box {
  padding: 40px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border-radius: 12px;
  text-align: center;
  font-size: 18px;
}
</style>

五、性能优化

5.1 GPU 加速

使用 transformopacity 属性触发 GPU 加速,避免引起重排(reflow):

/* 推荐:使用 transform 和 opacity */
.slide-enter-active {
  transition: transform 0.3s ease, opacity 0.3s ease;
}

.slide-enter-from {
  transform: translateX(100%);
  opacity: 0;
}

/* 避免:使用 width、height、top、left 等属性 */
/* 这些属性会引起重排,性能较差 */
.bad-example-enter-active {
  transition: width 0.3s ease; /* 不推荐 */
}

5.2 will-change 属性

对于复杂的动画,可以预先告知浏览器哪些属性将发生变化:

.animated-element {
  will-change: transform, opacity;
}

/* 动画完成后移除 will-change */
.animated-element.animation-complete {
  will-change: auto;
}

注意:will-change 会占用额外的内存,动画结束后应及时移除。

5.3 减少同时动画的元素数量

当列表中有大量元素需要同时动画时,考虑分批处理或使用虚拟列表:

<script setup>
import { ref, computed } from 'vue'

const items = ref([...Array(100).keys()])

// 分批显示动画
const visibleItems = computed(() => {
  // 只显示前 20 个,或根据滚动位置动态计算
  return items.value.slice(0, 20)
})
</script>

六、Vue2 vs Vue3 过渡对比

特性 Vue2 Vue3
进入开始类名 v-enter v-enter-from
离开开始类名 v-leave v-leave-from
进入结束类名 v-enter-to v-enter-to
离开结束类名 v-leave-to v-leave-to
TransitionGroup 标签 默认 span 默认 span
TransitionGroup tag 属性 支持 支持
appear 属性 支持 支持
mode 属性 in-out / out-in in-out / out-in
CSS 钩子 支持 支持
JS 钩子 支持 支持,增加了 onLeaveCancelled
类型支持 有限 完整的 TypeScript 支持

七、实战:完整的页面过渡系统

<!-- App.vue -->
<template>
  <div id="app">
    <nav>
      <RouterLink to="/">首页</RouterLink>
      <RouterLink to="/about">关于</RouterLink>
      <RouterLink to="/list">列表</RouterLink>
    </nav>
    
    <RouterView v-slot="{ Component }">
      <Transition name="page" mode="out-in">
        <component :is="Component" />
      </Transition>
    </RouterView>
  </div>
</template>

<style>
/* 页面过渡 */
.page-enter-active,
.page-leave-active {
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.page-enter-from {
  opacity: 0;
  transform: translateX(20px);
}

.page-leave-to {
  opacity: 0;
  transform: translateX(-20px);
}

/* 路由激活状态 */
.router-link-active {
  color: #42b983;
  font-weight: bold;
}
</style>

八、常见问题

Q1:为什么我的过渡动画不生效?

常见原因:

  1. 忘记给 v-for 的元素添加唯一的 :key
  2. CSS 类名写错(Vue3 使用 v-enter-from 而非 v-enter
  3. 过渡元素没有条件渲染(v-ifv-show
  4. CSS 选择器优先级不够

Q2:TransitionGroup 的 move 动画不生效?

确保:

  1. 设置了 .v-move
  2. 列表项有唯一的 :key
  3. 离开的元素设置了 position: absolute

Q3:如何在组件首次渲染时触发动画?

使用 appear 属性:

<Transition appear>
  <div>首次渲染也会触发动画</div>
</Transition>

Q4:mode=“out-in” 和 mode=“in-out” 有什么区别?

  • out-in:当前元素先离开,新元素再进入(推荐,避免两个元素同时存在)
  • in-out:新元素先进入,当前元素再离开

九、总结

Vue3 的过渡系统功能强大且易于使用:

组件 用途 关键特性
Transition 单元素/组件过渡 进入/离开类名、JS 钩子、mode
TransitionGroup 列表过渡 move 动画、tag 属性、FLIP

核心要点

  1. Vue3 将 v-enter 改为 v-enter-fromv-leave 改为 v-leave-from
  2. 优先使用 transformopacity 实现 GPU 加速
  3. TransitionGroup 需要唯一的 :key.v-move
  4. JavaScript 钩子适合与 GSAP 等库集成
  5. 合理使用 will-changemode 属性优化体验

十、练习题

  1. 实现一个手风琴(Accordion)组件,使用 Transition 实现展开/收起的平滑动画。

  2. 创建一个可拖拽排序的列表,使用 TransitionGroup 实现拖拽时的位置交换动画。

  3. 使用 GSAP 实现一个复杂的页面加载动画序列:Logo 先放大出现,然后标题从下方滑入,最后按钮淡入。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐