Skip to content

VueJS - 过渡与动画

在本章中,我们将探讨如何在元素被添加到 DOM(文档对象模型)、更新或移除时,为你的 VueJS 应用中的元素应用吸引人的过渡和动画效果。

Vue 提供了一个内置的 <transition> 包装组件。你可以将这个组件包裹在你希望在进入或离开 DOM 时(通常使用像 v-if、v-show 或动态组件等指令)应用过渡效果的任何元素或组件外部。

<transition name="my-transition">
<p v-if="show">Hello</p>
</transition>

当一个被 <transition> 包裹的元素插入或移除时,Vue 会在过渡的不同阶段自动添加/移除 CSS 类。name 属性(例如 my-transition)用作这些类的前缀。如果未提供 name,则前缀默认为 v-。

进入/离开过渡会应用以下六个类:

  • 进入阶段:
  • v-enter-from: 进入的起始状态。在元素被插入之前添加,在插入后一帧移除。
  • v-enter-active: 进入的活跃状态。在整个进入阶段应用。用于定义持续时间、延迟和缓动曲线。当过渡完成时移除。
  • v-enter-to: 进入的结束状态。在插入后一帧添加(与 v-enter-from 移除同时),当过渡完成时移除。
  • 离开阶段:
  • v-leave-from: 离开的起始状态。在触发离开过渡时立即添加,在一帧后移除。
  • v-leave-active: 离开的活跃状态。在整个离开阶段应用。用于定义持续时间、延迟和缓动曲线。当过渡完成时移除。
  • v-leave-to: 离开的结束状态。在触发离开过渡后一帧添加(与 v-leave-from 移除同时),当过渡完成时移除。

如果你使用了像 name="fade" 这样的 name 属性,则类名将是 .fade-enter-from、.fade-enter-active 等。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 3 Transition Example</title>
<script src="https://unpkg.com/vue@3"></script>
<style>
/* 元素的基本样式 */
.demo-p {
font-size: 20px;
color: darkcyan;
}
/* 定义过渡状态 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* 可选:如果需要,定义 enter-to 和 leave-from 状态,
通常 opacity: 1 是默认值 */
.fade-enter-to,
.fade-leave-from {
opacity: 1;
}
</style>
</head>
<body>
<div id="app">
<button @click="show = !show">Toggle Text</button>
<transition name="fade">
<p class="demo-p" v-if="show">Animation Example</p>
</transition>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const show = ref(true);
return { show };
}
}).mount('#app');
</script>
</body>
</html>

在此示例中:

  • 我们使用了 Vue 3 的 Composition API(createApp、ref)。
  • <p> 标签使用 v-if="show" 进行条件渲染。
  • 它被包裹在 <transition name="fade"> 中。
  • CSS 类 .fade-enter-active 和 .fade-leave-active 定义了过渡持续时间和缓动。
  • .fade-enter-from 和 .fade-leave-to 定义了元素不可见的状态(opacity 为 0)。

当点击 ‘Toggle Text’ 按钮时,show ref 会改变。如果 show 变为 true,段落会在 0.5 秒内淡入。如果 show 变为 false,它会淡出。

你也可以使用 CSS 动画代替过渡。与 <transition> 组件的使用方式类似,但 Vue 会监听 animationend 事件而不是 transitionend 事件。通常你在 *-enter-active 或 *-leave-active 类中定义动画。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 3 Animation Example</title>
<script src="https://unpkg.com/vue@3"></script>
<style>
.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);
}
}
.demo-div {
width: 100px;
height: 100px;
background-color: tomato;
border-radius: 50%;
margin-top: 20px;
}
</style>
</head>
<body>
<div id="app">
<button @click="show = !show">Toggle Ball</button>
<transition name="bounce">
<div class="demo-div" v-if="show"></div>
</transition>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const show = ref(true);
return { show };
}
}).mount('#app');
</script>
</body>
</html>

这里,我们定义了一个名为 bounce-in 的 @keyframes 动画。我们将此动画应用于 .bounce-enter-active 类。对于离开动画(.bounce-leave-active),我们应用相同的动画,但方向 reverse。当按钮切换时,红色圆圈会以弹跳效果出现或以反向弹跳消失。

你可以轻松集成第三方 CSS 动画库,例如 Animate.css。你可以直接使用特定属性向 <transition> 组件提供自定义类名,而无需自己定义过渡类。

  • enter-from-class
  • enter-active-class
  • enter-to-class
  • leave-from-class
  • leave-active-class
  • leave-to-class

这些属性的优先级高于常规类名,对于将 Vue 的过渡与 Animate.css 等库结合使用特别有用。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 3 Animate.css Example</title>
<!-- 使用较新版本的 Animate.css -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>
<script src="https://unpkg.com/vue@3"></script>
<style>
.demo-p {
font-size: 24px;
font-weight: bold;
color: steelblue;
}
</style>
</head>
<body>
<div id="app">
<button @click="show = !show">Toggle with Animate.css</button>
<transition
enter-active-class="animate__animated animate__fadeInDown"
leave-active-class="animate__animated animate__fadeOutUp"
>
<p class="demo-p" v-if="show">Using Animate.css!</p>
</transition>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const show = ref(true);
return { show };
}
}).mount('#app');
</script>
</body>
</html>

在这段代码中,我们引入了 Animate.css 库。然后,我们在 <transition> 组件上使用了 enter-active-class 和 leave-active-class 属性。我们提供了必要的 Animate.css 类(animate__animated 是必需的,加上特定的动画类如 animate__fadeInDown)。现在,切换段落会使用库中预定义的动画。

Vue 通过监听 transitionend 或 animationend 事件自动检测过渡或动画何时结束。但是,如果你有复杂的过渡(例如,CSS 过渡与持续时间不同的 CSS 动画混合),或者 Vue 无法正确检测到持续时间,你可以使用 duration prop 指定显式持续时间。

<!-- 进入和离开使用相同的持续时间 -->
<transition :duration="1000">...</transition>
<!-- 进入和离开使用不同的持续时间 -->
<transition :duration="{ enter: 500, leave: 800 }">...</transition>

持续时间以毫秒为单位指定。

对于更复杂的动画或 CSS 过渡/动画不足以处理的场景(例如,动画化 CSS 无法直接处理的元素属性,协调多个元素),你可以使用 JavaScript 钩子。你定义方法并将它们绑定到 <transition> 组件的事件上。

  • @before-enter(el)
  • @enter(el, done): 重要: 必须调用 done() 来表示进入动画结束。
  • @after-enter(el)
  • @enter-cancelled(el)
  • @before-leave(el)
  • @leave(el, done): 重要: 必须调用 done() 来表示离开动画结束。
  • @after-leave(el)
  • @leave-cancelled(el)(仅在使用 v-show 时可用)

仅使用 JavaScript 钩子(不使用 CSS 过渡/动画)时,一个好的做法是向 <transition> 组件添加 :css="false"。这会告诉 Vue 跳过 CSS 检测,可能会稍微提高性能并防止 CSS 规则干扰。

示例:使用 JS 钩子实现简单的透明度变化

Section titled “示例:使用 JS 钩子实现简单的透明度变化”
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 3 JS Hooks Example</title>
<script src="https://unpkg.com/vue@3"></script>
</head>
<body>
<div id="app">
<button @click="show = !show">Toggle with JS</button>
<transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@leave="onLeave"
:css="false" /* 告诉 Vue 我们通过 JS 处理动画 */
>
<p style="font-size: 20px; color: green;" v-if="show">
Animating with JavaScript!
</p>
</transition>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const show = ref(false);
const onBeforeEnter = (el) => {
// 在进入前设置初始状态
el.style.opacity = 0;
el.style.transform = 'translateY(20px)';
};
const onEnter = (el, done) => {
// 开始动画
// 使用简单的 requestAnimationFrame 进行演示
let start;
const duration = 500; // ms
function step(timestamp) {
if (start === undefined) start = timestamp;
const elapsed = timestamp - start;
const progress = Math.min(elapsed / duration, 1);
el.style.opacity = progress;
el.style.transform = `translateY(${20 * (1 - progress)}px)`;
if (progress < 1) {
requestAnimationFrame(step);
} else {
done(); // 标志动画完成
}
}
requestAnimationFrame(step);
};
const onLeave = (el, done) => {
// 开始离开动画(逻辑类似,反转透明度和 transform)
let start;
const duration = 300; // ms
function step(timestamp) {
if (start === undefined) start = timestamp;
const elapsed = timestamp - start;
const progress = Math.min(elapsed / duration, 1);
el.style.opacity = 1 - progress;
el.style.transform = `translateY(${20 * progress}px)`;
if (progress < 1) {
requestAnimationFrame(step);
} else {
done(); // 标志动画完成
}
}
requestAnimationFrame(step);
};
return { show, onBeforeEnter, onEnter, onLeave };
}
}).mount('#app');
</script>
</body>
</html>

此示例使用 requestAnimationFrame 进行了一个基本的 JavaScript 动画,控制元素的透明度和位置。像 GSAP(GreenSock Animation Platform)这样的库通常用于更复杂的 JS 动画,提供强大的补间功能和时间线控制。请记住,在 @enter 和 @leave 钩子中动画完成时调用 done 回调函数。

常见陷阱:在 @enter 和 @leave 钩子中忘记调用 done 回调函数将导致过渡无限期地挂起。

要在组件首次挂载(初始渲染)时应用过渡,请在 <transition> 组件上添加 appear 属性。

<transition name="fade" appear>
<p v-if="true">加载时淡入</p>
</transition>

你也可以使用 appear-active-class、appear-from-class、appear-to-class 等属性,或像 @before-appear、@appear、@after-appear、@appear-cancelled 等 JS 钩子,专门为初始渲染过渡指定自定义 CSS 类或 JavaScript 钩子。

当使用 v-if/v-else-if/v-else 在元素之间切换,或通过 <component :is="..."> 切换动态组件时,<transition> 组件也可以处理过渡。但是,一个常见的要求是两个元素必须具有唯一的 key 属性,以便 Vue 能正确地追踪并为其应用过渡。

<transition name="fade" mode="out-in">
<button v-if="isEditing" key="edit">Edit</button>
<button v-else key="save">Save</button>
</transition>

mode 属性控制 timing:out-in(当前元素先过渡离开,然后新元素过渡进入)或 in-out(新元素先过渡进入,然后当前元素过渡离开)。通常建议使用 out-in 以获得更流畅的视觉效果。

为了动画化使用 v-for 渲染的列表,Vue 提供了 <TransitionGroup> 组件。它会渲染一个真实的 DOM 元素(默认为 <span>,可通过 tag prop 配置),并将过渡应用于其内部的元素。

与 <transition> 的主要区别:

  • 它会渲染一个实际的元素(例如 <ul>、<div>),而不是抽象的。
  • 过渡类应用于内部元素,而不是 group 元素本身。
  • 列表中的每个项都必须具有唯一的 key 属性。
  • 它支持移动过渡(v-move 类),用于列表顺序改变时,这需要使用 CSS transform。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 3 TransitionGroup Example</title>
<script src="https://unpkg.com/vue@3"></script>
<style>
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(30px);
}
/* 确保离开的元素脱离文档流,以便正确计算移动动画。 */
.list-leave-active {
position: absolute;
}
</style>
</head>
<body>
<div id="app">
<button @click="addItem">Add Item</button>
<button @click="removeItem">Remove Item</button>
<transition-group name="list" tag="ul">
<li v-for="item in items" :key="item.id" class="list-item">
{{ item.text }}
</li>
</transition-group>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const items = ref([
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' }
]);
let nextId = 4;
const addItem = () => {
const randomIndex = Math.floor(Math.random() * items.value.length);
items.value.splice(randomIndex, 0, { id: nextId++, text: `Item ${nextId -1}` });
};
const removeItem = () => {
if (items.value.length > 0) {
const randomIndex = Math.floor(Math.random() * items.value.length);
items.value.splice(randomIndex, 1);
}
};
return { items, addItem, removeItem };
}
}).mount('#app');
</script>
</body>
</html>

此示例演示了如何通过淡入淡出和滑动过渡来添加和移除列表项。注意每个 li 上的 key 使用以及 <transition-group> 上的 tag="ul" prop。.list-leave-active { position: absolute; } 样式对于在移除项目时实现平滑的移动动画非常重要。

进一步阅读:Vue 官方文档提供了关于过渡的深入细节:https://vuejs.org/guide/built-ins/transition.html 以及列表过渡:https://vuejs.org/guide/built-ins/transition-group.html