Appearance
过渡动画
TIP
本小节对应的项目源代码,可以去 刘龙彬/vue3-component-study-4 仓库中下载。
Vue 提供了 <Transition>
和 <TransitionGroup>
两个内置组件,来帮助程序员制作基于状态变化的过渡和动画。除此之外,Vue 还支持其它的动画实现方式,例如:切换 CSS 的 class 或用状态绑定样式来驱动动画。
Transition
<Transition>
是一个内置组件,它无需注册即可在 Vue 中使用。它可以将进入和离开动画应用到通过默认插槽传递给它的元素或组件上。进入或离开可以由以下的条件之一触发:
- 由
v-if
所触发的切换 - 由
v-show
所触发的切换 - 由特殊元素
<component>
切换的动态组件 - 改变特殊的
key
属性
基于 CSS 的过渡效果
进入动画
定义布尔值的响应式数据,控制元素的显示和隐藏:
vue
<script setup>
import { ref } from 'vue'
// 布尔值,控制元素的显示和隐藏
const isShow = ref(false)
</script>
点击按钮切换布尔值的状态,并且使用 <Transition>
组件把需要应用过渡动画的元素包裹起来:
vue
<template>
<!-- 点击按钮,切换布尔值 -->
<button @click="isShow = !isShow">toggle</button>
<!-- 需要应用“进入/离开”动画的元素,需要被包裹到 <Transition> 中, -->
<!-- 而且 <Transition> 下只能放置单个节点 -->
<Transition>
<p v-if="isShow">hello liulongbin.</p>
</Transition>
</template>
使用 Vue 内置的三个 class 类,控制进入动画的起始状态、结束状态以及过渡效果:
vue
<style scoped>
/* 使用 .v-enter-from 类,
设置进入动画的“起始状态” */
.v-enter-from {
opacity: 0;
}
/* 使用 .v-enter-to 类,
设置进入动画的“结束状态” */
.v-enter-to {
opacity: 1;
}
/* 使用 .v-enter-active 类,
设置进入动画的“过渡效果”(要过渡的属性、时长、速度曲线) */
.v-enter-active {
transition: opacity 1s ease;
}
</style>
离开动画
一般情况下,离开动画和进入动画的效果是相反的(进入时 opacity
从 0-1,离开时 opacity
从 1-0)。因此,我们可以按照如下的方式,声明离开动画的 class 类名:
vue
<style scoped>
/* 使用 .v-enter-from 类,
设置进入动画的“起始状态” */
.v-enter-from,
.v-leave-to {
opacity: 0;
}
/* 使用 .v-enter-to 类,
设置进入动画的“结束状态” */
.v-enter-to,
.v-leave-from {
opacity: 1;
}
/* 使用 .v-enter-active 类,
设置进入动画的“过渡效果”(要过渡的属性、时长、速度曲线) */
.v-enter-active,
.v-leave-active {
transition: opacity 1s ease;
}
</style>
简化写法
如果进入动画的结束状态就是元素正常显示时的状态,则 .v-enter-to
的 class 可以省略。如果离开动画的起始状态就是元素正常显示时的状态,则 .v-leave-from
的 class 可以省略。

因此在刚才的例子中,我们可以把 .v-enter-to, .v-leave-from
对应的样式省略掉。改造后的 <style>
如下:
vue
<style scoped>
/* 使用 .v-leave-to 类,
设置离开动画的“结束状态” */
.v-enter-from,
.v-leave-to {
opacity: 0;
}
/* 省略 .v-enter-to 和 .v-leave-from 的 class */
/* 使用 .v-leave-active 类,
设置离开动画的“过渡效果”(要过渡的属性、时长、速度曲线) */
.v-enter-active,
.v-leave-active {
transition: opacity 1s ease;
}
</style>
自定义入场动画结束时的状态
如果我们希望元素进入时的 opacity
从 0 过渡到 0.5,此时我们需要把 .v-enter-to, .v-leave-from
选择器中的 opacity
修改为 0.5
:
vue
<style scoped>
/* 使用 .v-leave-to 类,
设置离开动画的“结束状态” */
.v-enter-from,
.v-leave-to {
opacity: 0;
}
/* 设置进入动画的“结束状态”,和离开动画的“起始状态” */
.v-enter-to,
.v-leave-from {
opacity: 0.5;
}
/* 使用 .v-leave-active 类,
设置离开动画的“过渡效果”(要过渡的属性、时长、速度曲线) */
.v-enter-active,
.v-leave-active {
transition: opacity 1s ease;
}
</style>
经过测试后我们发现,进入动画期间 opacity
从 0 过渡到 0.5 的过程是流畅的,但是过渡结束之后,元素的 opacity
会瞬间从 0.5 跳跃至 1。
发生这种情况的主要原因是:进入动画过渡结束之后,会从元素身上移除 .v-enter-to
的 class,相应的 opacity: 0.5
的样式也被移除了,就产生了跳跃的问题。
为了解决上述问题,我们可以直接给元素应用一个自定义的 class .custom-trans
。这样哪怕 .v-enter-to
类在过渡结束后被从元素上移除了,也能够保证元素的 opacity
值为 0.5
:
html
<Transition>
<p v-if="isShow" class="custom-trans">hello liulongbin.</p>
</Transition>
配套的 <style>
如下:
vue
<style scoped>
/* 用于兜底的 class 样式 */
.custom-trans {
opacity: 0.5;
}
/* 使用 .v-leave-to 类,
设置离开动画的“结束状态” */
.v-enter-from,
.v-leave-to {
opacity: 0;
}
/* 使用 .v-leave-from 类,
设置离开动画的“起始状态” */
.v-enter-to,
.v-leave-from {
opacity: 0.5;
}
/* 使用 .v-leave-active 类,
设置离开动画的“过渡效果”(要过渡的属性、时长、速度曲线) */
.v-enter-active,
.v-leave-active {
transition: opacity 1s ease;
}
</style>
同时过渡多个属性
在刚才的案例中,我们只是过渡了元素的 opacity
,如果我们想同时过渡元素的 opacity
和 font-size
属性,刚才的案例中的 <style>
可以改造如下:
vue
<style scoped>
/* 入场动画结束后,元素的正常展示状态 */
.custom-trans {
opacity: 0.5;
font-size: 50px;
}
/* 入场动画的起始状态 & 离场动画的结束状态 */
.v-enter-from,
.v-leave-to {
opacity: 0;
font-size: 12px;
}
/* 入场动画的结束状态 & 离场动画的起始状态 */
.v-enter-to,
.v-leave-from {
opacity: 0.5;
font-size: 50px;
}
/* 入场动画和离场动画的“过渡效果”(要过渡的属性、时长、速度曲线) */
.v-enter-active,
.v-leave-active {
transition: opacity 1s ease, font-size 1s ease;
}
</style>
为过渡效果命名
默认情况下 <Transition>
组件需要配合 6 个 class
实现动画效果。这 6 个 class 的类名是固定的,它们都已 v-
前缀开头:

我们可以给 <Transition>
组件传一个 name
prop 来声明一个过渡效果名:
html
<Transition name="fade">
<p v-if="isShow" class="custom-trans">hello liulongbin.</p>
</Transition>
此时这 6 个 class
类名的 v-
前缀需要替换为 fade-
前缀:
vue
<style scoped>
/* 入场动画结束后,元素的正常展示状态 */
.custom-trans {
opacity: 0.5;
font-size: 50px;
}
/* 入场动画的起始状态 & 离场动画的结束状态 */
.fade-enter-from,
.fade-leave-to {
opacity: 0;
font-size: 12px;
}
/* 入场动画的结束状态 & 离场动画的起始状态 */
.fade-enter-to,
.fade-leave-from {
opacity: 0.5;
font-size: 50px;
}
/* 入场动画和离场动画的“过渡效果”(要过渡的属性、时长、速度曲线) */
.fade-enter-active,
.fade-leave-active {
transition: opacity 1s ease, font-size 1s ease;
}
</style>
TIP
为什么要给过渡效果命名呢?
这可以让我们在一个组件中使用多个 <Transition>
组件,从而控制多个互不相关的元素的过渡效果。
CSS 的 animation
<Transition>
除了配合 CSS 的 transition
属性实现过渡动画,还可以配合 CSS 的 animation
属性实现关键帧动画。
vue
<script setup>
import { ref } from 'vue'
const flag = ref(false)
</script>
<template>
<button @click="flag = !flag">Toggle</button>
<Transition>
<h1 v-if="flag">Hello escook~~~😍</h1>
</Transition>
</template>
<style scoped>
/* 定义关键帧动画,命名为 bounce-in */
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.25);
}
100% {
transform: scale(1);
}
}
h1 {
text-align: center;
}
/* 入场动画 */
.v-enter-active {
animation: bounce-in 1s ease; /* 引用关键帧动画 */
}
/* 离场动画 */
.v-leave-active {
animation: bounce-in 1s ease reverse; /* 引用关键帧动画,并反转动画效果 */
}
</style>
TIP
我们同样可以使用 <Transition>
的 name
prop 为动画效果命名。
例如 name="bounce"
,此时入场/离场动画的类名需要分别修改为 .bounce-enter-active
和 .bounce-leave-active
。
自定义过渡 class
我们还可以借助于第三方的 CSS 动画库(例如 Animate.css)所提供的 class 类名,快速实现过渡动画效果。此时,我们需要向 <Transition>
传递以下的 props 来指定自定义的过渡 class,传入的这些 class 会覆盖相应阶段的默认 class 名:
enter-from-class
enter-active-class
enter-to-class
leave-from-class
leave-active-class
leave-to-class
使用 Animate.css 提供的动画效果控制进入和离开的动画时,首先需要在项目中安装 animate.css
:
bash
npm install animate.css --save
紧接着,在 main.js
中导入 animate.css
:
js
import 'animate.css'
最后通过 <Transition>
组件的 enter-active-class
属性提供进入的动画 class,通过 leave-active-class
属性提供离开的动画 class:
vue
<script setup>
import { ref } from 'vue'
const flag = ref(false)
</script>
<template>
<button @click="flag = !flag">Toggle</button>
<Transition class="animate__animated" enter-active-class="animate__zoomIn" leave-active-class="animate__zoomOut">
<h1 v-if="flag">你好!刘龙彬。</h1>
</Transition>
</template>
<style scoped>
h1 {
text-align: center;
}
</style>
🚨 注意 🚨
必须提供动画的基础类 animate__animated
。
JavaScript 钩子
<Transition>
组件除了能够实现 CSS 过渡和关键帧动画之外,还能使用 JavaScript 来控制动画的效果。<Transition>
提供了以下 8 个与动画有关的事件,我们可以在不同的事件中对动画进行细节上的控制:
html
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@enter-cancelled="onEnterCancelled"
@before-leave="onBeforeLeave"
@leave="onLeave"
@after-leave="onAfterLeave"
@leave-cancelled="onLeaveCancelled"
>
<!-- ... -->
</Transition>
js
// 在元素被插入到 DOM 之前被调用
// 用这个来设置元素的 "enter-from" 状态
function onBeforeEnter(el) {}
// 在元素被插入到 DOM 之后的下一帧被调用
// 用这个来开始进入动画
function onEnter(el, done) {
// 调用回调函数 done 表示过渡结束
// 如果与 CSS 结合使用,则这个回调是可选参数
done()
}
// 当进入过渡完成时调用。
function onAfterEnter(el) {}
// 当进入过渡在完成之前被取消时调用
function onEnterCancelled(el) {}
// 在 leave 钩子之前调用
// 大多数时候,你应该只会用到 leave 钩子
function onBeforeLeave(el) {}
// 在离开过渡开始时调用
// 用这个来开始离开动画
function onLeave(el, done) {
// 调用回调函数 done 表示过渡结束
// 如果与 CSS 结合使用,则这个回调是可选参数
done()
}
// 在离开过渡完成、
// 且元素已从 DOM 中移除时调用
function onAfterLeave(el) {}
// 仅在 v-show 过渡中可用
function onLeaveCancelled(el) {}
🚨 注意 🚨
我们没必要搞懂所有的动画钩子函数的作用,应该以需求来驱动学习,仅在需要时去翻阅官方文档查漏补缺即可。
配合 GSAP 实现钩子动画
GSAP 是一个简单易用功能强大的 JS 动画库。它提供了一系列的函数和插件,能帮我们实现复杂的 JS 动画效果。运行如下的命令在项目中安装它:
bash
npm install gsap@3.12.5 --save
接下来,让我们基于它实现一个简单的元素淡入淡出的动画效果。首先,封装 GsapFade.vue
组件,使用布尔值 flag
控制元素的切换:
vue
<script setup>
import { ref } from 'vue'
const flag = ref(false)
</script>
<template>
<button @click="flag = !flag">Toggle</button>
<!-- 使用 :css="false" 跳过探测 CSS 的过渡动画,防止 CSS 规则意外干扰 JS 动画 -->
<Transition :css="false">
<h1 v-if="flag">Hello GSAP.</h1>
</Transition>
</template>
监听 <Transition>
的 @before-enter
事件,设置入场动画开始前的初始状态;
监听 <Transition>
的 @enter
事件,设置入场动画最终要达到的效果;
监听 <Transition>
的 @leave
事件,设置离场动画最终要达到的效果;
使用 gsap.set()
设置入场动画开始前元素的初始状态,使用 gsap.to()
设置元素要达成的动画效果:
vue
<script setup>
import { gsap } from 'gsap'
import { ref } from 'vue'
const flag = ref(false)
const onBeforeEnter = (el) => {
// 入场动画开始前,元素的初始状态
gsap.set(el, {
opacity: 0,
color: 'rgba(0, 0, 0)',
marginLeft: 0
})
}
const onEnter = (el, done) => {
// 入场动画最终要达到的效果
gsap.to(el, {
opacity: 1,
color: 'rgba(255, 0, 0)',
marginLeft: 100,
duration: 1, // 单位:秒
ease: 'elastic',
// 一定要在动画完成之后,调用 done 函数,
// 通知 Vue 入场动画结束了
onComplete: done
})
}
const onLeave = (el, done) => {
// 离场动画最终要达到的效果
gsap.to(el, {
opacity: 0,
duration: 1,
color: 'rgba(0, 0, 255)',
marginLeft: 0,
ease: 'back',
// 一定要在动画完成之后,调用 done 函数,
// 通知 Vue 离场动画结束了
onComplete: done
})
}
</script>
<template>
<button @click="flag = !flag">Toggle</button>
<!-- 使用 :css="false" 跳过探测 CSS 的过渡动画,防止 CSS 规则意外干扰 JS 动画 -->
<Transition :css="false" @before-enter="onBeforeEnter" @enter="onEnter" @leave="onLeave">
<h1 v-if="flag">Hello GSAP.</h1>
</Transition>
</template>
配合 GSAP 实现打字机效果
封装 GsapTypeWriter.vue
组件,并实现基础的布局和元素的切换:
vue
<script setup>
import { ref } from 'vue'
const flag = ref(false)
</script>
<template>
<button @click="flag = !flag">Toggle</button>
<Transition>
<h1 class="title" v-if="flag"></h1>
</Transition>
</template>
<style scoped>
.title {
font-family: Gilroy, 阿里巴巴普惠体;
margin: 80px 0 0 60px;
font-size: 50px;
}
.title::before {
content: 'Call me ';
color: #000;
}
</style>
使用 gsap
+ TextPlugin
实现单个字符串的打字机效果:
vue
<script setup>
// 1. 导入 gsap 和文本动画插件
import { gsap } from 'gsap'
import { TextPlugin } from 'gsap/TextPlugin'
import { ref } from 'vue'
const flag = ref(false)
// 2. 关闭曲线动画效果(保证打字机效果是匀速的线性动画)
gsap.defaults({ ease: 'none' })
// 3. 为 gsap 注册文本动画插件
gsap.registerPlugin(TextPlugin)
const onEnter = (el, done) => {
// delay: 入场动画的延迟时长(s)
// repeat:动画重复执行多少次
// yoyo:是否开启动画的逆向执行(yoyo 会消耗1次 repeat 次数)
// repeatDelay:动画重复执行时,延迟的时长(s)
gsap.timeline({ delay: 0.5, repeat: 1, yoyo: true, repeatDelay: 0.5 }).to(el, {
text: 'Liu Longbin.',
// 每次动画的时长(s)
duration: 2,
// repeat 动画结束之后要调用 done
onReverseComplete: done
})
}
</script>
<template>
<button @click="flag = !flag">Toggle</button>
<Transition :css="false" @enter="onEnter">
<h1 class="title" v-if="flag"></h1>
</Transition>
</template>
<style scoped>
.title {
font-family: Gilroy, 阿里巴巴普惠体;
margin: 80px 0 0 60px;
font-size: 50px;
color: green;
}
.title::before {
content: 'Call me ';
color: #000;
}
</style>
封装生成 timeline 的 genTextWriterTL()
函数,并使用嵌套的 gsap.timeline()
实现多个字符串的打字机效果:
vue
<script setup>
import { gsap } from 'gsap'
import { TextPlugin } from 'gsap/TextPlugin'
import { ref } from 'vue'
const flag = ref(false)
gsap.defaults({ ease: 'none' })
gsap.registerPlugin(TextPlugin)
// 1. 封装一个根据“字符串”生成 timeline 动画的函数
const genTextWriterTL = (el, done, text, color = '#000') => {
// delay: 入场动画的延迟时长(s)
// repeat:动画重复执行多少次
// yoyo:是否开启动画的逆向执行(yoyo 会消耗1次 repeat 次数)
// repeatDelay:动画重复执行时,延迟的时长(s)
return gsap
.timeline({ delay: 0.5, repeat: 1, yoyo: true, repeatDelay: 0.5 })
.set(el, { color }) // 设置文本的颜色值
.to(el, {
text,
// 根据文字的长度,动态计算每次动画的时长(s)
duration: (2 / 12) * text.length,
// repeat 动画结束之后要调用 done
onReverseComplete: done
})
}
const onEnter = (el, done) => {
// 2. 实现多个 timeline 动画的嵌套执行
// repeat: -1 表示无限循环的 timeline 动画
const tl = gsap.timeline({ repeat: -1 })
// 调用 tl.add() 方法添加嵌套的 timeline 动画
tl.add(genTextWriterTL(el, done, 'Liu Longbin.', 'rgb(34,111,222)'))
.add(genTextWriterTL(el, done, 'ESCOOK.', 'rgb(0, 162, 16)'))
.add(genTextWriterTL(el, done, '刘龙彬。', '#ff00ff'))
}
</script>
<template>
<button @click="flag = !flag">Toggle</button>
<Transition :css="false" @enter="onEnter">
<h1 class="title" v-if="flag"></h1>
</Transition>
</template>
<style scoped>
.title {
font-family: Gilroy, 阿里巴巴普惠体;
margin: 80px 0 0 60px;
font-size: 50px;
color: green;
}
.title::before {
content: 'Call me ';
color: #000;
}
</style>
出现时过渡
有的动画效果需要在元素渲染时自动执行。比如刚才的打字机动画,我们希望它能够自动执行,而非用户点击按钮才能执行打字机动画。此时我们可以给 <Transition>
组件添加 appear
属性即可:
html
<Transition appear> ... </Transition>
因此,要实现打字机动画的自动执行,我们需要把布尔值 flag
相关的代码移除掉,然后给 Transition
添加 appear
属性:
vue
<script setup>
import { gsap } from 'gsap'
import { TextPlugin } from 'gsap/TextPlugin'
gsap.defaults({ ease: 'none' })
gsap.registerPlugin(TextPlugin)
const genTextWriterTL = (el, done, text, color = '#000') => {
return gsap
.timeline({ delay: 0.5, repeat: 1, yoyo: true, repeatDelay: 0.5 })
.set(el, { color })
.to(el, {
text,
duration: (2 / 12) * text.length,
onReverseComplete: done
})
}
const onEnter = (el, done) => {
const tl = gsap.timeline({ repeat: -1 })
tl.add(genTextWriterTL(el, done, 'Liu Longbin.', 'rgb(34,111,222)'))
.add(genTextWriterTL(el, done, 'ESCOOK.', 'rgb(0, 162, 16)'))
.add(genTextWriterTL(el, done, '刘龙彬。', '#ff00ff'))
}
</script>
<template>
<!-- 1. 添加 appear 属性,即可在元素渲染出来后自动执行动画 -->
<Transition appear :css="false" @enter="onEnter">
<!-- 2. 这里的 h1 不需要使用 v-if 进行控制 -->
<h1 class="title"></h1>
</Transition>
</template>
<style scoped>
.title {
font-family: Gilroy, 阿里巴巴普惠体;
margin: 80px 0 0 60px;
font-size: 50px;
color: green;
}
.title::before {
content: 'Call me ';
color: #000;
}
</style>
深层级过渡
默认情况下,<Transition>
组件的过渡效果会作用于其直接子元素。我们还可以通过相关的配置,把过渡效果应用于更深层的子元素上。
JavaScript 深层级过渡
在 @enter
和 @leave
的事件处理函数中,第一个形参 el
指向 <Transition>
的直接子元素。我们可以调用 el.querySelector()
API,查找 el 下嵌套的深层子元素,并将 JavaScript 动画效果应用于深层子元素上。例如,打字机案例可以改造如下:
vue
<script setup>
import { gsap } from 'gsap'
import { TextPlugin } from 'gsap/TextPlugin'
gsap.defaults({ ease: 'none' })
gsap.registerPlugin(TextPlugin)
// 1. 封装一个根据“字符串”生成 timeline 动画的函数
const genTextWriterTL = (el, done, text, color = '#000') => {
// delay: 入场动画的延迟时长(s)
// repeat:动画重复执行多少次
// yoyo:是否开启动画的逆向执行(yoyo 会消耗1次 repeat 次数)
// repeatDelay:动画重复执行时,延迟的时长(s)
return gsap
.timeline({ delay: 0.5, repeat: 1, yoyo: true, repeatDelay: 0.5 })
.set(el, { color }) // 设置文本的颜色值
.to(el, {
text,
// 根据文字的长度,动态计算每次动画的时长(s)
duration: (2 / 12) * text.length,
// repeat 动画结束之后要调用 done
onReverseComplete: done
})
}
const onEnter = (el, done) => {
// 调用 DOM API,查询 el 下的子元素 #type-writer,并把动画效果应用于查询到的子元素
// 请注意:这里的 el 是 h1,也就是 <Transition> 直接嵌套的那个元素
const animateEl = el.querySelector('#type-writer')
const tl = gsap.timeline({ repeat: -1 })
tl.add(genTextWriterTL(animateEl, done, 'Liu Longbin.', 'rgb(34,111,222)'))
.add(genTextWriterTL(animateEl, done, 'ESCOOK.', 'rgb(0, 162, 16)'))
.add(genTextWriterTL(animateEl, done, '刘龙彬。', '#ff00ff'))
}
</script>
<template>
<Transition appear :css="false" @enter="onEnter">
<h1 class="title">
<span>Call me </span>
<!-- 希望把过渡效果应用于 id="type-writer" 的 span 元素上 -->
<span id="type-writer"></span>
</h1>
</Transition>
</template>
<style scoped>
.title {
font-family: Gilroy, 阿里巴巴普惠体;
margin: 80px 0 0 60px;
font-size: 50px;
color: #000;
}
.title::before {
/* content: 'Call me '; */
color: #000;
}
</style>
CSS 深层级过渡
核心要素
一定要给 <Transition>
组件绑定 :duration
总过渡时长的毫秒值,否则过渡动画无法准确实现。
通过 CSS 的后代选择器,我们可以把过渡效果应用到特定的后代元素上,而非根元素上。例如 <Transition>
中的元素结构如下:
html
<Transition name="escook">
<h1 class="outer" v-if="flag">
<span>Hello </span>
<span class="inner">Liu Longbin.</span>
</h1>
</Transition>
我们希望把1 秒后淡入淡出的过渡效果应用到 .inner
的 span 元素上。示例代码如下:
vue
<script setup>
import { ref } from 'vue'
const flag = ref(false)
</script>
<template>
<button @click="flag = !flag">Toggle</button>
<!-- 2. 因此这里的“入场/离场”过渡时长应该是 1000 毫秒 -->
<Transition name="escook" :duration="1000">
<h1 class="outer" v-if="flag">
<span>Hello </span>
<span class="inner">Liu Longbin.</span>
</h1>
</Transition>
</template>
<style scoped>
.escook-enter-from .inner,
.escook-leave-to .inner {
opacity: 0;
}
.escook-enter-to .inner,
.escook-leave-from .inner {
opacity: 1;
}
.escook-enter-active .inner,
.escook-leave-active .inner {
/* 1. 入场和离场的动画都是 1s */
transition: all 1s ease;
}
</style>
duration 的完整写法
在刚才的代码示例中 :duration="1000"
是简化过后的写法,它代表的含义是:入场动画的总过渡时长为 1s、离场动画的总过渡时长也为 1s。它的完整写法如下:
html
<!-- 2. 因此这里的“入场/离场”过渡时长应该是 1000 毫秒 -->
<Transition name="escook" :duration="{ enter: 1000, leave: 1000 }"> ... </Transition>
其中 enter
用来指定入场动画的总时长,leave
用来指定离场动画的总时长。
🚨 注意 🚨
动画总时长的计算方法是 transition-duration
的时长加上 transition-delay
的时长。
例如,下面的代码中分别指定了 enter 和 leave 的动画总时长:
vue
<script setup>
import { ref } from 'vue'
const flag = ref(false)
</script>
<template>
<button @click="flag = !flag">Toggle</button>
<!-- 3. 因此这里需要分别指定“入场/离场”动画的总过渡时长 -->
<Transition name="escook" :duration="{ enter: 1000, leave: 1500 }">
<h1 class="outer" v-if="flag">
<span>Hello </span>
<span class="inner">Liu Longbin.</span>
</h1>
</Transition>
</template>
<style scoped>
.escook-enter-from .inner,
.escook-leave-to .inner {
opacity: 0;
}
.escook-enter-to .inner,
.escook-leave-from .inner {
opacity: 1;
}
.escook-enter-active .inner,
.escook-leave-active .inner {
/* 1. 入场和离场的动画 duration 都是 1s */
transition: all 1s ease;
}
.escook-leave-active .inner {
/* 2. 但是,离场动画有一个 0.5s 的 delay 延迟 */
transition-delay: 0.5s;
}
</style>
带延迟的过渡动画序列
目前,只有 <span class="inner">Liu Longbin.</span>
这个元素拥有过渡动画,而 <span>Hello </span>
由于没有过渡动画,因此它的入场/离场就很生硬。
为此,我们可以创建带延迟的过渡动画序列:先让 .outer
的元素从左向右平移出现,平移结束后再让 .inner
的元素淡入。我们发现 .inner
的动画效果要推迟到 .outer
的动画完成之后再开始。
示例代码如下:
vue
<script setup>
import { ref } from 'vue'
const flag = ref(false)
</script>
<template>
<button @click="flag = !flag">Toggle</button>
<!-- 1.5 总入场时长为:根元素的入场动画时长 + 子元素的入场动画时长 -->
<!-- 2.2 总离场时长为:根元素的离场动画时长 + 子元素的离场动画时长 -->
<Transition name="escook" :duration="{ enter: 2000, leave: 2500 }">
<h1 class="outer" v-if="flag">
<span>Hello </span>
<span class="inner">Liu Longbin.</span>
</h1>
</Transition>
</template>
<style scoped>
/* 1.1 设置根元素入场前、离场后的位置 */
.escook-enter-from,
.escook-leave-to {
transform: translateX(-100px);
}
/* 1.2 设置根元素入场后、离场前的位置 */
.escook-enter-to,
.escook-leave-from {
transform: translateX(0px);
}
/* 1.3 设置根元素入场、离场的动画效果和时长 */
.escook-enter-active {
transition: all 1s ease;
}
/* 2.1 根元素的离场动画,应该在子元素的离场动画之后,
因此,根元素的动画要延迟到子元素的动画结束之后,
需要延迟的总时长为:子元素的离场等待时长(0.5s) + 子元素的离场动画时长(1s) */
.escook-leave-active {
transition-delay: 1.5s;
}
.escook-enter-from .inner,
.escook-leave-to .inner {
opacity: 0;
}
.escook-enter-to .inner,
.escook-leave-from .inner {
opacity: 1;
}
.escook-enter-active .inner,
.escook-leave-active .inner {
transition: all 1s ease;
}
/* 1.4 子元素的入场,要在根元素动画结束之后,因此需要添加 delay 延迟 */
.escook-enter-active .inner {
transition-delay: 1s;
}
.escook-leave-active .inner {
/* 子元素离场前的延迟时长 */
transition-delay: 0.5s;
}
</style>
可复用的过渡效果
通过 Props 实现过渡效果的复用
我们可以把“打字机”案例进行封装,让用户把前缀和文字内容以 Props 的形式传入组件中:
html
<CommonTypeWriter prefix="Call me " :texts="textList" />
数据源为:
js
const textList = [
// 每个对象必须包含 text 属性,color 属性是可选的
{ text: 'Liu Longbin.', color: 'rgb(34,111,222)' },
{ text: '彬果锅~' },
{ text: 'ESCOOK.', color: 'rgb(0, 162, 16)' },
{ text: '刘龙彬。', color: '#ff00ff' }
]
<CommonTypeWriter>
组件封装如下:
vue
<script setup>
// 1. 声明 prefix 和 texts 两个 Prop
const props = defineProps({
prefix: {
type: String,
default: 'Call me '
},
texts: {
type: Array,
required: true,
validator(value) {
// 判断数组长度
if (value.length === 0) {
console.warn('texts 数组不能为空!')
return false
}
// 判断数组元素的属性
const haveTextProp = value.every((item) => item.text)
if (!haveTextProp) {
console.warn('texts 数组中的每一项必须包含 text 属性!')
}
// return true 表示校验通过;return false 表示校验不通过。
return haveTextProp
}
}
})
import { gsap } from 'gsap'
import { TextPlugin } from 'gsap/TextPlugin'
gsap.defaults({ ease: 'none' })
gsap.registerPlugin(TextPlugin)
const genTextWriterTL = (el, done, text, color = '#000') => {
return gsap
.timeline({ delay: 0.5, repeat: 1, yoyo: true, repeatDelay: 0.5 })
.set(el, { color })
.to(el, {
text,
duration: (2 / 12) * text.length,
onReverseComplete: done
})
}
const onEnter = (el, done) => {
const animateEl = el.querySelector('#type-writer')
const tl = gsap.timeline({ repeat: -1 })
// 3. 循环向 tl 中添加嵌套的 timeline 动画
props.texts.forEach((item) => {
tl.add(genTextWriterTL(animateEl, done, item.text, item.color || '#000'))
})
}
</script>
<template>
<Transition appear :css="false" @enter="onEnter">
<h1 class="title">
<!-- 2. 渲染前缀 -->
<span>{{ prefix }}</span>
<span id="type-writer"></span>
</h1>
</Transition>
</template>
<style scoped>
.title {
font-family: Gilroy, 阿里巴巴普惠体;
margin: 80px 0 0 60px;
font-size: 50px;
color: #000;
}
.title::before {
color: #000;
}
</style>
通过 Slot 实现过渡效果的复用
除了可以使用 Props 实现动画效果的复用之外,我们还可以使用 Slot 插槽实现动画的复用:把需要添加过渡效果的元素以插槽内容的形式传入封装好的动画组件中。
例如,把“带延迟的过渡动画序列”组件,封装为可复用的动画组件:
vue
<script setup>
// 定义名为 flag 和 appear 的 Prop 并给定默认值
defineProps({
flag: {
type: Boolean,
default: true // 默认展示元素
},
appear: {
type: Boolean,
default: false // 元素展示时,默认不应用过渡效果
}
})
</script>
<template>
<Transition name="escook" :duration="{ enter: 2000, leave: 2500 }" :appear>
<div class="outer" v-if="flag">
<!-- 1. 前缀的插槽 -->
<slot name="prefix" />
<div class="inner">
<!-- 2. 内容的插槽。请注意外层的新增了 class="inner" 的 div 容器,它是动画的挂载点 -->
<slot name="content" />
</div>
</div>
</Transition>
</template>
<style scoped>
/* 3. 把 div 设置为行内元素 */
.inner {
display: inline;
}
.escook-enter-from,
.escook-leave-to {
transform: translateX(-100px);
}
.escook-enter-to,
.escook-leave-from {
transform: translateX(0px);
}
.escook-enter-active,
.escook-leave-active {
transition: all 1s ease;
}
.escook-leave-active {
transition-delay: 1.5s;
}
.escook-enter-from .inner,
.escook-leave-to .inner {
opacity: 0;
}
.escook-enter-to .inner,
.escook-leave-from .inner {
opacity: 1;
}
.escook-enter-active .inner,
.escook-leave-active .inner {
transition: all 1s ease;
}
.escook-enter-active .inner {
transition-delay: 1s;
}
.escook-leave-active .inner {
transition-delay: 0.5s;
}
</style>
改造完毕之后,我们可以通过传入插槽内容的形式复用这个动画组件:
html
<!-- 纯文本节点的过渡 -->
<DeepTrans appear>
<template #prefix>你好啊~</template>
<template #content>
<span>露水姑娘。</span>
</template>
</DeepTrans>
<hr />
<!-- 带有图片的过渡 -->
<DeepTrans appear>
<template #prefix>Hello,</template>
<template #content>
<br />
<img src="./assets/logo.svg" alt="" width="100" />
</template>
</DeepTrans>
元素间的过渡
可以使用 v-if
配合 v-else
、v-else-if
控制多个元素之间的切换。我们唯一要确保的事情就是同一时刻只有一个结点被渲染(因为 <Transition>
仅支持单根节点的过渡动画):
vue
<script setup>
import { ref } from 'vue'
const hero = ref('姜子牙')
</script>
<template>
<button @click="hero = '姜子牙'">姜子牙</button>
<button @click="hero = '老夫子'">老夫子</button>
<button @click="hero = '孙尚香'">孙尚香</button>
<button @click="hero = '廉颇'">廉颇</button>
<Transition class="animate__animated" enter-active-class="animate__fadeInLeft" leave-active-class="animate__fadeOutRight">
<h1 v-if="hero === '姜子牙'">不刷新世界观怎么成长。</h1>
<h1 v-else-if="hero === '老夫子'">教学生,顺便拯救世界。</h1>
<h1 v-else-if="hero === '孙尚香'">大小姐驾到!统统闪开!</h1>
<h1 v-else-if="hero === '廉颇'">伤痕,是岁月,是成长,是力量。</h1>
</Transition>
</template>
<style scoped>
h1 {
font-family: 阿里巴巴普惠体;
font-size: 20px;
}
</style>
过渡模式
在刚才的案例中,我们发现元素在切换的过程中,位置会有闪烁的情况。为了解决此问题,我们可以给元素添加 position: absolute
定位:
css
h1 {
font-family: 阿里巴巴普惠体;
position: absolute;
}
此时我们发现新旧元素的过渡是同时进行的。如果您想在旧元素的离场动画结束后再执行新元素的入场动画,则需要为 <Transition>
添加 mode="out-in"
的过渡模式:
html
<Transition mode="out-in" enter-active-class="animate__animated animate__fadeInLeft" leave-active-class="animate__animated animate__fadeOutRight">
<h1 v-if="hero === '姜子牙'">不刷新世界观怎么成长。</h1>
<h1 v-else-if="hero === '老夫子'">教学生,顺便拯救世界。</h1>
<h1 v-else-if="hero === '孙尚香'">大小姐驾到!统统闪开!</h1>
<h1 v-else-if="hero === '廉颇'">伤痕,是岁月,是成长,是力量。</h1>
</Transition>
组件间的过渡
<Transition>
也可用于“动态组件”之间的切换,需要强调的是:被过渡的组件必须有唯一根元素,否则过渡效果可能无法正常工作:
vue
<script setup>
import { ref } from 'vue'
import Home from '@/components3/TransHome.vue'
import Movie from '@/components3/TransMovie.vue'
import About from '@/components3/TransAbout.vue'
const index = ref(0)
const coms = [Home, Movie, About]
</script>
<template>
<button @click="index = 0">Home</button>
<button @click="index = 1">Movie</button>
<button @click="index = 2">About</button>
<hr />
<Transition class="animate__animated" enter-active-class="animate__fadeInLeft" leave-active-class="animate__fadeOutRight">
<component :is="coms[index]"></component>
</Transition>
</template>
<style scoped>
:global(body) {
overflow-x: hidden;
}
.container {
position: absolute;
}
</style>
动态过渡
动态过渡指的是动态为 <Transition>
组件绑定 name
、enter-active-class
、leave-active-class
等这些 prop 的值。从而让我们可以根据状态的变化,动态的应用不同类型的过渡。语法格式如下:
html
<Transition :name="transitionName">
<!-- ... -->
</Transition>
例如我们可以改造组件间过渡的案例,根据索引的变化动态设置离场/入场动画的方向:
vue
<script setup>
import { ref, watch } from 'vue'
import Home from '@/components3/TransHome.vue'
import Movie from '@/components3/TransMovie.vue'
import About from '@/components3/TransAbout.vue'
const index = ref(0)
const coms = [Home, Movie, About]
// 1.1 普通数据:向左离场的类名对象
const leaveLeft = {
'enter-active-class': 'animate__fadeInRight',
'leave-active-class': 'animate__fadeOutLeft'
}
// 1.2 普通数据:向右离场的类名对象
const leaveRight = {
'enter-active-class': 'animate__fadeInLeft',
'leave-active-class': 'animate__fadeOutRight'
}
// 2. 响应式数据对象:包含了离场和入场动画的类名
const transClassObj = ref({})
watch(index, (newIndex, oldIndex) => {
if (newIndex > oldIndex) {
// 3.1 向左离场
transClassObj.value = leaveRight
} else {
// 3.2 向右离场
transClassObj.value = leaveLeft
}
})
</script>
<template>
<button @click="index = 0">Home</button>
<button @click="index = 1">Movie</button>
<button @click="index = 2">About</button>
<hr />
<!-- 4. 把响应式数据对象上的每个属性,绑定到 <Transition> 上 -->
<Transition class="animate__animated container" v-bind="transClassObj">
<component :is="coms[index]"></component>
</Transition>
</template>
<style scoped>
:global(body) {
overflow-x: hidden;
}
.container {
position: absolute;
}
</style>
使用 Key Attribute 过渡
有时为了触发过渡,你需要强制重新渲染 DOM 元素。以“秒数计数器” TimerCounter.vue
组件为例:
vue
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import 'animate.css'
const count = ref(new Date().getSeconds())
let timerId = null
onMounted(() => {
timerId = setInterval(() => {
count.value = new Date().getSeconds()
}, 1000)
})
onBeforeUnmount(() => {
clearInterval(timerId)
})
</script>
<template>
<div class="container">
<Transition class="animate__animated" enter-active-class="animate__fadeInUp" leave-active-class="animate__fadeOutUp">
<h1 class="num" :key="count">{{ count }}</h1>
</Transition>
</div>
</template>
<style scoped>
.container {
position: absolute;
left: 50%;
top: 50%;
border: 1px solid #efefef;
font-family: Gilroy;
height: 100px;
line-height: 100px;
width: 150px;
transform: translate(-50%, -50%);
border-radius: 10px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.1));
box-shadow: 1px 1px 20px rgba(0, 0, 0, 0.1);
}
.num {
position: absolute;
margin: 0;
width: 100%;
height: 100%;
text-align: center;
font-size: 50px;
}
</style>
如果不使用 key
attribute,则只有文本节点会被更新,元素节点没有变化,因此不会发生过渡。
但是,有了 key
属性,Vue 就知道在 count
改变时创建一个新的 span
元素,因此 Transition
组件有两个不同的元素在它们之间进行过渡。
拓展 - 动画时钟
首先,把刚才的 TimerCounter.vue
组件进行封装,把 count
定义为 Prop 数据:
vue
<script setup>
defineProps({
count: {
type: Number,
required: true,
default: 0
}
})
</script>
<template>
<div class="container">
<Transition enter-active-class="animate__animated animate__fadeInUp" leave-active-class="animate__animated animate__fadeOutUp">
<h1 class="num" :key="count">{{ count.toString().padStart(2, '0') }}</h1>
</Transition>
</div>
</template>
<style scoped>
.container {
position: relative;
border: 1px solid #efefef;
font-family: Gilroy;
height: 100px;
line-height: 100px;
width: 150px;
border-radius: 10px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.1));
box-shadow: 1px 1px 20px rgba(0, 0, 0, 0.1);
}
.num {
position: absolute;
margin: 0;
width: 100%;
height: 100%;
text-align: center;
font-size: 50px;
}
</style>
接下来,基于 TimerCounter
组件进行二次封装,定义动画时钟的组件 AnimateTimer.vue
组件如下:
vue
<script setup>
import TimerCounter from './components3/TimerCounter.vue'
import { ref } from 'vue'
// 初始化数据
const dt = new Date()
const h = ref(dt.getHours())
const m = ref(dt.getMinutes())
const s = ref(dt.getSeconds())
// 在定时器中更新时分秒
setInterval(() => {
const dt = new Date()
h.value = dt.getHours()
m.value = dt.getMinutes()
s.value = dt.getSeconds()
}, 1000)
</script>
<template>
<div class="container">
<div class="time-box">
<!-- 复用 TimerCounter 组件,向下传入 count prop 的值 -->
<TimerCounter :count="h" />
<span class="sep">:</span>
<TimerCounter :count="m" />
<span class="sep">:</span>
<TimerCounter :count="s" />
</div>
</div>
</template>
<style scoped>
.container {
display: flex;
height: 100%;
justify-content: center;
align-items: center;
}
.time-box {
display: flex;
height: 100px;
}
.sep {
font-size: 50px;
line-height: 80px;
margin: 0 15px;
}
</style>
TransitionGroup
<TransitionGroup>
是一个内置组件,用于对 v-for
列表中的元素或组件的插入、移除和顺序改变添加动画效果。
列表的进入/离开动画
实现元素在随机索引上的新增和删除
核心要点:
- 根据
Math.floor(Math.random() * list.value.length)
获取随机的索引; - 根据
next
获取下一个要新增的数字;
vue
<script setup>
import { ref } from 'vue'
const list = ref([1, 2, 3, 4, 5])
let next = 6
// 随机位置,新增元素
const add = () => {
list.value.splice(Math.floor(Math.random() * list.value.length), 0, next++)
}
// 随机位置,删除元素
const remove = () => {
if (list.value.length === 0) return
list.value.splice(Math.floor(Math.random() * list.value.length), 1)
}
</script>
<template>
<button @click="add">Add Item</button>
<button @click="remove">Remove Item</button>
<ul>
<li v-for="item in list" :key="item">{{ item }}</li>
</ul>
</template>
实现新增和删除元素时的过渡
使用 <TransitionGroup>
组件替代 ul
元素,并为 <TransitionGroup>
组件提供 tag="ul"
属性。然后再通过 name
prop 自定义过渡的名称:
html
<TransitionGroup tag="ul" name="escook">
<li v-for="item in list" :key="item">{{ item }}</li>
</TransitionGroup>
自定义 CSS 过渡的类名如下:
vue
<style scoped>
.escook-enter-from,
.escook-leave-to {
transform: translateX(50px);
opacity: 0;
}
.escook-enter-active,
.escook-leave-active {
transition: all 0.5s ease;
}
</style>
移动动画
上面的示例有一些明显的缺陷:当某一项被插入或移除时,它周围的元素会立即发生“跳跃”而不是平稳地移动。我们可以通过添加一些额外的 CSS 规则来解决这个问题:
vue
<style scoped>
.escook-enter-from,
.escook-leave-to {
transform: translateX(50px);
opacity: 0;
}
.escook-move, /* 新增 or 删除元素时:对移动中的元素应用过渡 */
.escook-enter-active,
.escook-leave-active {
transition: all 0.5s ease;
}
/* 移除元素时:把将要被移除的元素从布局流中删除,以便能够正确地计算移动的动画。 */
.escook-leave-active {
position: absolute;
}
</style>
甚至,我们还可以对整个列表项的顺序进行洗牌,也能够得到流畅的动画效果:
html
<button @click="shuffle">shuffle</button>
对应的 JavaScript 处理逻辑为:
js
const shuffle = () => {
if (list.value.length <= 1) return
list.value.forEach((item, i) => {
// 获取随机索引值
const randomIndex = Math.floor(Math.random() * list.value.length)
// 把当前索引的值,替换为随机索引对应的值
list.value[i] = list.value[randomIndex]
// 把随机索引的值,替换为当前循环项的值
list.value[randomIndex] = item
})
}
渐进延迟列表动画
实现基本的列表搜索功能
在数据框中填写搜索关键词之后,通过计算属性动态返回搜索的结果,并把搜索的结果渲染到模板中:
vue
<!-- TransListDelay.vue -->
<script setup>
import { ref, computed } from 'vue'
// 所有数据项的列表
const list = ref(['Bruce Lee', 'Jackie Chan', 'Chuck Norris', 'Jet Li', 'Kung Fury'])
// 搜索关键词
const kw = ref('')
// 计算属性:根据关键词,返回搜索的结果
const searchList = computed(() => list.value.filter((x) => x.toLowerCase().includes(kw.value.toLowerCase())))
</script>
<template>
<input type="text" v-model="kw" />
<hr />
<ul>
<li v-for="item in searchList" :key="item">{{ item }}</li>
</ul>
</template>
实现搜索列表的延迟动画效果
核心思路
控制列表元素的 opacity 和 height,来实现动画效果;通过 gsap 的 delay 属性,控制列表项动画的延迟。
使用 <TransitionGroup tag="ul">
代替 <ul>
元素,并为其绑定 @before-enter="onBeforeEnter"
、@enter="onEnter"
、@leave="onLeave"
动画事件:
html
<TransitionGroup tag="ul" @before-enter="onBeforeEnter" @enter="onEnter" @leave="onLeave">
<li v-for="(item, i) in searchList" :key="item" :data-index="i">{{ item }}</li>
</TransitionGroup>
在 <script setup>
中导入 gsap
:
js
import { gsap } from 'gsap'
定义如下 3 个动画事件的处理函数:
js
// -------动画处理函数-------
const onBeforeEnter = (el) => {
el.style.opacity = 0
el.style.height = 0
}
const onEnter = (el, done) => {
gsap.to(el, {
opacity: 1,
height: '1.6em',
delay: el.dataset.index * 0.15,
onComplete: done
})
}
const onLeave = (el, done) => {
gsap.to(el, {
opacity: 0,
height: 0,
delay: el.dataset.index * 0.15,
onComplete: done
})
}
其它动画技巧
基于侦听器的数字动画
实现不含动画的基础功能
使用 ref 定义两个响应式数据 from
和 to
。其中 to
表示需要更新到的值,它和 input 输入框进行 v-model
的双向数据绑定。from
表示更新前页面上显示的旧值,它需要逐步更新到 from
的值,更新的过程需要使用动画实现:
vue
<script setup>
import { ref, watch } from 'vue'
const from = ref(0)
const to = ref(0)
watch(to, (nextValue) => {
from.value = nextValue
})
</script>
<template>
<input type="text" v-model="to" />
<h1>{{ from }}</h1>
</template>
基于 gsap.to() 实现数字动画
调用 gsap.to()
方法实现数字动画,需要传入两个参数:
js
gsap.to(要修改的数据对象, { duration: 动画持续时长, 要修改的属性: 新值 })
修改代码如下:
vue
<script setup>
import { ref, watch } from 'vue'
// 导入 gsap
import { gsap } from 'gsap'
const from = ref(0)
const to = ref(0)
watch(to, (nextValue) => {
// from 是要修改的数据对象
// 要修改 from 对象的 .value 属性的值
// from.value 属性的新值是 nextValue
gsap.to(from, { duration: 0.5, value: nextValue })
})
</script>
<template>
<input type="text" v-model.lazy="to" />
<!-- 过渡时,不保留小数位 -->
<h1>{{ from.toFixed(0) }}</h1>
</template>
基于状态驱动的动画
元素的样式属性可由响应式状态的变化进行控制,再通过 CSS 的 transition
控制过渡效果即可:
vue
<script setup>
import { ref } from 'vue'
const len = ref(300)
</script>
<template>
<input type="text" v-model.lazy="len" />
<div class="box"></div>
</template>
<style scoped>
.box {
/* 1. 动态绑定元素的宽和高 */
width: v-bind('len + "px"');
height: v-bind('len + "px"');
background-color: #ff00ff;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
/* 2. 添加过渡动画 */
transition: all 0.5s cubic-bezier(1, 0.26, 0.97, 0.66);
}
</style>
TIP
贝塞尔曲线生成器:https://cubic-bezier.com/