Vue 动画:transition 组件与动画库
Vue 提供了强大的内置过渡系统,通过 transition 组件,你可以轻松实现元素进入、离开时的动画效果。掌握这套机制后,还能无缝集成第三方动画库,实现更复杂的交互效果。
理解 transition 组件的核心机制
transition 组件的本质是一个容器,它会感知内部元素的挂载和卸载过程,并自动在恰当的时机为元素添加 CSS 类名。这些类名对应动画的不同阶段,你只需定义对应的 CSS 样式即可触发动画。
当元素进入 DOM 时,Vue 会依次添加 v-enter-from、v-enter-active、v-enter-to 三个类名。当元素离开时,则会添加 v-leave-from、v-leave-active、v-leave-to。其中 v-enter-active 和 v-leave-active 贯穿整个动画过程,适合用来定义动画的持续时间、缓动函数等通用属性。
基础用法:纯 CSS 动画
首先来看最简单的情况:为单个元素的显示与隐藏添加淡入淡出效果。
<template>
<button @click="show = !show">切换显示</button>
<transition name="fade">
<p v-if="show">这是一段需要动画效果的文本</p>
</transition>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(true)
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
解释:点击按钮切换 show 的值,p 元素会根据条件渲染或移除。transition 组件监听到这个变化,自动应用 CSS 类名。opacity 从 0 到 1(或反向)产生淡入淡出效果。
如果你需要在元素插入时应用不同的动画进入方式,比如从上方滑入,可以这样写:
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide-fade-enter-from {
transform: translateY(-20px);
opacity: 0;
}
.slide-fade-leave-to {
transform: translateY(20px);
opacity: 0;
}
注意进入和离开动画可以使用不同的缓动函数。ease-out 适合进入动画,因为它让元素快速出现后缓慢稳定;cubic-bezier 提供了更细腻的物理质感。
JavaScript 钩子函数
有时候纯 CSS 无法满足复杂动画需求,比如需要根据动画进度做计算、与其他库协同,或者动画逻辑本身就很复杂。这时可以使用 Vue 提供的 JavaScript 钩子函数。
transition 组件暴露了八个钩子:@before-enter、@enter、@after-enter、@enter-cancelled,以及对应的离开版本 @before-leave、@leave、@after-leave、@leave-cancelled。在 enter 和 leave 钩子中,你必须调用 done 回调函数来通知 Vue 动画已完成,否则 Vue 会认为动画仍在进行中。
<template>
<transition
@enter="onEnter"
@leave="onLeave"
:css="false"
>
<div v-if="show">动画元素</div>
</transition>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(true)
const onEnter = (el, done) => {
// el 是被动画的元素
// done 是必须调用的回调函数
el.animate([
{ transform: 'scale(0)' },
{ transform: 'scale(1)' }
], {
duration: 300,
easing: 'ease-out'
}).onfinish = done
}
const onLeave = (el, done) => {
el.animate([
{ transform: 'scale(1)' },
{ transform: 'scale(0)' }
], {
duration: 300,
easing: 'ease-in'
}).onfinish = done
}
</script>
:css="false" 是关键设置。它告诉 Vue 不要尝试操作 CSS 类名,完全由 JavaScript 控制动画。这样做能稍微提升性能,因为 Vue 不需要管理额外的 class。
列表动画:transition-group
当需要为列表中的每个元素添加动画时,应该使用 transition-group 组件。它与 transition 的核心区别在于:列表中的每个元素都是独立的动画单元,并且支持添加和移除元素时的重排动画。
<template>
<button @click="addItem">添加项目</button>
<button @click="removeItem">移除项目</button>
<transition-group name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.content }}
</li>
</transition-group>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, content: '项目一' },
{ id: 2, content: '项目二' },
{ id: 3, content: '项目三' }
])
let nextId = 4
const addItem = () => {
items.value.push({ id: nextId++, content: `项目${nextId - 1}` })
}
const removeItem = () => {
items.value.pop()
}
</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-move {
transition: transform 0.5s ease;
}
</style>
```
`list-move` 类是 `transition-group` 的特殊类名。当列表中的元素位置发生变化时,Vue 会自动为受影响的元素添加这个类,产生平滑的移动动画。这是实现列表拖拽排序动画效果的基础。
需要注意的是,`leave-active` 中的元素在离开动画期间仍会占用空间。如果希望元素离开后立即释放空间,可以给离开的元素添加 `position: absolute`:
```css
.list-leave-active {
position: absolute;
}
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
```
但这会导致列表宽度不稳定,更稳妥的做法是配合 JavaScript 钩子,在动画结束后再移除元素。
---
## 集成 GSAP 动画库
GSAP(GreenSock Animation Platform)是业界最强大的 Web 动画库之一。它解决了不同浏览器间动画 API 不一致的问题,提供了时间线控制、滚动触发等高级功能。将 GSAP 与 Vue transition 组件结合,能大幅提升开发效率。
### 基础集成
在 `enter` 钩子中使用 GSAP:
```javascript
import gsap from 'gsap'
const onEnter = (el, done) => {
gsap.fromTo(el,
{ opacity: 0, y: 50 },
{
opacity: 1,
y: 0,
duration: 0.5,
ease: 'power2.out',
onComplete: done
}
)
}
const onLeave = (el, done) => {
gsap.to(el, {
opacity: 0,
y: -50,
duration: 0.3,
ease: 'power2.in',
onComplete: done
})
}
```
### 时间线控制
GSAP 的时间线(Timeline)功能允许你编排多个动画的先后顺序,这在复杂场景中非常有用:
```javascript
const onEnter = (el, done) => {
const tl = gsap.timeline({ onComplete: done })
tl.fromTo(el,
{ opacity: 0, scale: 0.8 },
{ opacity: 1, scale: 1, duration: 0.3, ease: 'back.out(1.7)' }
)
.fromTo(el.querySelector('.inner'),
{ opacity: 0, y: 20 },
{ opacity: 1, y: 0, duration: 0.2 }
)
}
```
`back.out(1.7)` 中的 `1.7` 是回弹幅度。值越大,元素到达终点后会 overshoot(超出)后再回弹,产生轻微的弹性效果。
### 列表动画实战
使用 GSAP 实现更精致的列表动画:
```javascript
const onEnter = (el, done) => {
gsap.fromTo(el,
{ opacity: 0, x: -50 },
{
opacity: 1,
x: 0,
duration: 0.4,
ease: 'power3.out',
onComplete: done,
delay: el.dataset.index * 0.1 // 依次延迟进入
}
)
}
const onLeave = (el, done) => {
gsap.to(el, {
opacity: 0,
x: 50,
duration: 0.3,
ease: 'power2.in',
onComplete: done
})
}
```
`delay: el.dataset.index * 0.1` 让列表元素依次延迟进入,形成瀑布流效果。注意在模板中为每个元素绑定 `data-index` 属性:
```html
<transition-group
@enter="onEnter"
@leave="onLeave"
:css="false"
tag="ul"
>
<li
v-for="(item, index) in items"
:key="item.id"
:data-index="index"
>
{{ item.content }}
</li>
</transition-group>
```
---
## 集成 Anime.js
Anime.js 是另一个轻量且功能强大的动画库,以其简洁的 API 和出色的性能著称。
### 基础用法
```javascript
import anime from 'animejs/lib/anime.es.js'
const onEnter = (el, done) => {
anime({
targets: el,
opacity: [0, 1],
translateY: [30, 0],
easing: 'easeOutExpo',
duration: 600,
complete: done
})
}
const onLeave = (el, done) => {
anime({
targets: el,
opacity: [1, 0],
translateY: [0, -30],
easing: 'easeInExpo',
duration: 400,
complete: done
})
}
```
Anime.js 的数组语法(如 `translateY: [30, 0]`)表示从 30 过渡到 0,这在定义起始状态时非常方便。
### 关键帧动画
Anime.js 支持关键帧,让你能精确控制动画的每个阶段:
```javascript
const onEnter = (el, done) => {
anime({
targets: el,
keyframes: [
{ opacity: 0, scale: 0.5, duration: 0 },
{ opacity: 1, scale: 1.2, duration: 200, easing: 'easeOutQuad' },
{ scale: 1, duration: 150, easing: 'easeOutBounce' }
],
complete: done
})
}
```
这个动画包含三个阶段:初始状态、快速放大并淡入、回弹到正常大小。关键帧让复杂动画的实现变得直观可控。
---
## 性能优化与最佳实践
### 使用 will-change 提示浏览器
对于复杂动画,提前告知浏览器哪些属性即将变化,能让浏览器进行优化准备:
```css
.animated-element {
will-change: transform, opacity;
}
```
但要注意:`will-change` 会占用额外内存,不要过度使用或在动画结束后忘记移除。
### 优先使用 transform 和 opacity
浏览器的合成线程能高效处理 `transform` 和 `opacity` 的变化,不会触发重排(reflow)或重绘(repaint)。尽可能让动画只涉及这两个属性,其他属性的动画性能开销会大得多。
### 避免在动画过程中读取布局信息
在 JavaScript 动画中,如果触发布局信息读取(如 `offsetHeight`、`getBoundingClientRect`),会导致浏览器强制同步重排,严重影响性能。常见的解决方法是预先读取并缓存这些值:
```javascript
const onEnter = (el, done) => {
const height = el.offsetHeight // 只读取一次
gsap.fromTo(el,
{ height: 0 },
{ height: height, duration: 0.3, onComplete: done }
)
}
```
### 设置 :css="false"
当完全使用 JavaScript 控制动画时,务必设置 `:css="false"`。这能阻止 Vue 添加和移除 CSS 类,避免潜在的性能损耗和意外的样式冲突。
---
## 实战:表单验证反馈动画
结合 Vue 的响应式系统和动画能力,可以为表单验证创建直观的反馈效果:
```html
<template>
<form @submit.prevent="submitForm">
<div class="form-group">
<input
v-model="email"
:class="{ error: emailError }"
@blur="validateEmail"
placeholder="输入邮箱"
/>
<transition name="shake">
<span v-if="emailError" class="error-msg">
请输入有效的邮箱地址
</span>
</transition>
</div>
<button type="submit">提交</button>
</form>
</template>
<script setup>
import { ref } from 'vue'
import gsap from 'gsap'
const email = ref('')
const emailError = ref(false)
const validateEmail = () => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
emailError.value = !regex.test(email.value)
}
const submitForm = () => {
validateEmail()
if (!emailError.value) {
// 提交逻辑
console.log('表单提交成功')
}
}
</script>
<style scoped>
.shake-enter-active {
animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
}
@keyframes shake {
10%, 90% { transform: translate3d(-1px, 0, 0); }
20%, 80% { transform: translate3d(2px, 0, 0); }
30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
40%, 60% { transform: translate3d(4px, 0, 0); }
}
.error {
border-color: #ff4d4f;
}
.error-msg {
color: #ff4d4f;
font-size: 12px;
}
</style>
当用户输入无效邮箱并离开输入框时,错误信息会以抖动动画的形式出现,有效吸引用户注意力。
选择合适的工具
纯 CSS 动画适合简单的状态切换,如淡入淡出、滑动等,开发成本低、性能好。JavaScript 钩子配合 GSAP 适合需要精细控制的复杂动画,尤其当动画具有时序依赖或需要动态计算时。Anime.js 则在轻量级场景和关键帧动画中表现优秀。
根据项目实际需求选择合适的方案,不必过度设计。多数情况下,CSS 动画配合 Vue 内置的 transition 组件已经足够应对日常开发需求。

暂无评论,快来抢沙发吧!