一、概述

先来看两个概念,了解过渡与动画:

过渡:元素可动画属性的计算值产生变化时,需要以怎样的方式过渡到目标值。可以定义过渡时间和过渡函数来控制过渡动画的效果,默认行为是瞬间切换成变化后的结果,即无动画。

动画:使用 @keyframes 预定义一系列动画进程中的关键帧,关键帧中可以指定任意可动画属性的值,然后只需要应用动画到你想要的元素上并指定时间,次数,缓动函数,状态,动画结束后的行为等。

二者都会让你的页面元素动起来,区别在于:

过渡(Transition)

  • 需要事件触发,比如 hoverclick 等;
  • 一次性的;
  • 只能定义开始和结束状态,不能定义中间状态;

动画(Animation)

  • 不需要事件触发;
  • 显示地随着时间的流逝,周期性的改变元素的 CSS 属性值,区别于一次性。
  • 通过百分比来定义过程中的不同形态,可以很细腻。

二、忆往昔

我们先来简单回顾一下在CSS中如何实现过渡与动画效果。

1. Transition in CSS

  1. <script setup lang="ts">
  2. import { reactive } from 'vue';
  3. const classNames = reactive({
  4. transition: false
  5. });
  6. const onButtonTap = () => {
  7. classNames.transition = !classNames.transition;
  8. }
  9. </script>
  10. <template>
  11. <div class="box" :class="classNames"></div>
  12. <button type="button" @click="onButtonTap">Toggle</button>
  13. </template>
  14. <style scoped>
  15. .box {
  16. width: 100px;
  17. height: 100px;
  18. margin-bottom: 16px;
  19. background-color: red;
  20. transition: .5s background-color linear;
  21. }
  22. .transition {
  23. background-color: blue;
  24. }
  25. </style>

演示效果:

trans_in_css.gif

2. Animation in CSS

  1. <script setup lang="ts">
  2. import { reactive } from 'vue';
  3. const classNames = reactive({
  4. ani: false,
  5. });
  6. const onButtonTap = () => {
  7. classNames.ani = true;
  8. };
  9. </script>
  10. <template>
  11. <div class="box" :class="classNames"></div>
  12. <button type="button" @click="onButtonTap">启用动画</button>
  13. </template>
  14. <style scoped>
  15. @keyframes ani {
  16. to {
  17. transform: translateX(300px);
  18. background-color: blue;
  19. }
  20. }
  21. .box {
  22. width: 100px;
  23. height: 100px;
  24. margin-bottom: 16px;
  25. background-color: red;
  26. }
  27. .ani {
  28. animation: ani 2s linear 1 forwards;
  29. }
  30. </style>

演示效果:

ani_in_css.gif

三、过渡 & 动画

接下来,我们一起了解在 Vue 中,如何使用过渡与动画。

1. 基本使用

Vue 内置组件 <transition /> 在下面情况中,可以给任何元素和组件添加进入和离开动画。

  • 条件渲染:v-if 或者 v-show
  • 动态组件:component
  • 组件根节点

2. 过渡类名

在进入/离开的过渡中,会有 6 个 class 切换。

类名 描述
v-enter-from 定义进入过渡的开始状态
v-enter-active 定义进入过渡生效时的状态
v-enter-to 定义进入过渡的结束状态
v-leave-from 定义离开过渡的开始状态
v-leave-active 定义离开过渡生效时的状态
v-leave-to 离开过渡的结束状态

注意:假设 transition 设有 name 属性,class 名将 v- 替换为 name 属性值-

比如:<transition name="fade >",那么 v-enter-from 将被替换为 fade-enter-from,以此类推。

transitions.svg

3. 单元素/组件过渡

3.1. CSS 过渡

CSS 过渡是最常用的过渡类型之一,举例:

  1. <script setup lang="ts">
  2. import { ref } from 'vue';
  3. const visible = ref(true);
  4. </script>
  5. <template>
  6. <button type="button" @click="visible = !visible">Toggle</button>
  7. <transition name="slide-fade">
  8. <div v-show="visible" class="box"></div>
  9. </transition>
  10. </template>
  11. <style scoped>
  12. .box {
  13. width: 100px;
  14. height: 100px;
  15. margin-top: 16px;
  16. background-color: red;
  17. }
  18. /* 可以为进入和离开动画设置不同的持续时间和动画函数 */
  19. .slide-fade-enter-active {
  20. transition: all 0.75s ease-out;
  21. }
  22. .slide-fade-leave-active {
  23. transition: all 1s cubic-bezier(1, 0.5, 0.8, 1);
  24. }
  25. .slide-fade-enter-from,
  26. .slide-fade-leave-to {
  27. transform: translateX(300px);
  28. opacity: 0;
  29. }
  30. </style>

效果演示:

transition-01.gif

上述示例,点击 Toggle 按钮,切换元素显示状态,使得元素向右位移 300 像素,透明逐渐为0隐藏元素,呈现元素效果相反。

3.2. CSS 动画

CSS 动画用法同 CSS 过渡,区别是在动画中 v-enter-from 类在节点插入 DOM 后不会立即移除,而是在 animationend 事件触发时移除。

  1. <script setup lang="ts">
  2. import { ref } from 'vue';
  3. const visible = ref(true);
  4. </script>
  5. <template>
  6. <button type="button" @click="visible = !visible">Toggle</button>
  7. <transition name="bounce">
  8. <div v-show="visible" class="box"></div>
  9. </transition>
  10. </template>
  11. <style scoped>
  12. .box {
  13. width: 100px;
  14. height: 100px;
  15. margin-top: 16px;
  16. background-color: red;
  17. }
  18. .bounce-enter-active {
  19. animation: bounce-in 0.5s;
  20. }
  21. .bounce-leave-active {
  22. animation: bounce-in 0.5s reverse;
  23. }
  24. @keyframes bounce-in {
  25. 0% {
  26. transform: scale(0);
  27. }
  28. 50% {
  29. transform: scale(1.25);
  30. }
  31. 100% {
  32. transform: scale(1);
  33. }
  34. }
  35. </style>

代码解读:

1)上述示例中,通过 @keyframes 定义 bounce-in 动画,该动画从0开始缩放到1.25倍再调整到1倍,所以有会一种从无到有,先放大再缩回原始尺寸的效果。

2)隐藏元素时同样使用 bounce-in 动画,不过增加了 reverse 关键字,该关键字的作用和显示的动画刚好相反,让预定义动画反向执行。

效果演示:

animation-01.gif

3.3. 自定义类名 & animate.css

我们可以通过以下属性来自定义过渡类名:

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

它们的优先级高于普通的类名,当你希望将其它第三方 CSS 动画库与 Vue 的过度系统相结合时十分有用,比如 Animate.css

接下来我们尝试使用 Animate.css:

Steps 1:安装 animate.css

  1. $ npm install animate.css

Steps 2:导入

  1. import 'animate.css'

Steps 3:打开 Animate.css >> 官网,选择效果并复制效果类名(class name)

animate_css_guide.png

应用示例:

  1. <h1 class="animate__animated animate__bounce">An animated element</h1>

提示:animate__animated 这个 className 一定要 加上,不能省略

Steps 4: 编写代码,粘贴效果类名(class name)

  1. <script setup lang="ts">
  2. import { ref } from 'vue';
  3. const visible = ref(true);
  4. </script>
  5. <template>
  6. <button type="button" @click="visible = !visible">Toggle</button>
  7. <transition
  8. enter-active-class="animate__animated animate__bounceIn"
  9. leave-active-class="animate__animated animate__slideOutRight"
  10. >
  11. <h1 v-show="visible">Animate.css</h1>
  12. </transition>
  13. </template>

效果演示:

animation-css.gif

3.4. 同时使用过渡和动画

Vue 为了知道过渡何时完成,必须设置相应的事件监听器。它可以是 @transitionend@animationend,这取决于给元素应用的 CSS 规则。如果你只使用了其中一种,Vue 能自动识别其正确类型。

但是,在一些场景中,你需要给同一个元素同时设置两种过渡动效,比如有一个通过 Vue 触发的 CSS 动画,并且在悬停时结合一个 CSS 过渡。在这种情况中,你就需要使用 type 属性并设置 animationtransition 来显式声明你需要 Vue 监听的类型。

3.5. 显性的过渡持续时间

Vue 在 <transition> 组件上提供 duration 属性显式指定过渡持续时间 (以毫秒计):

  1. <transition :duration="1000">...</transition>

你也可以分别指定进入和离开的持续时间:

  1. <transition :duration="{ enter: 500, leave: 800 }">...</transition>

3.6. JavaScript 钩子函数

可以在 属性 中声明 JavaScript 钩子:

  1. <transition
  2. @before-enter="beforeEnter"
  3. @enter="enter"
  4. @after-enter="afterEnter"
  5. @enter-cancelled="enterCancelled"
  6. @before-leave="beforeLeave"
  7. @leave="leave"
  8. @after-leave="afterLeave"
  9. @leave-cancelled="leaveCancelled"
  10. :css="false"
  11. >
  12. <!-- ... -->
  13. </transition>
  • 和之前在 CSS 中的类名类似,这些钩子函数会在过渡到了对应阶段调用;
  • cancelled 是在过程中撤销操作,才会回调;
  • enterleave 对应的钩子函数有两个参数:
    • el:参与动画的元素;
    • done:过渡过程是否完成;
  • css:false:使元素设置的动画 CSS 失效;

4. 初始渲染的过渡

可以通过 appear 属性设置节点在 初始渲染(即页面在初始化的时候就执行一次动画) 的过渡:

  1. <transition appear>
  2. <!-- ... -->
  3. </transition>

5、多元素过渡

对于原生标签可以使用 v-if/v-else 。最常见的多标签过渡是一个列表和描述这个列表为空消息的元素:

  1. <transition>
  2. <table v-if="items.length > 0">
  3. <!-- ... -->
  4. </table>
  5. <p v-else>Sorry, no items found.</p>
  6. </transition>

实际上,通过使用 v-if/v-else-if/v-else 或将单个元素绑定到一个动态属性,可以在任意数量的元素之间进行过渡。例如:

  1. <transition>
  2. <button v-if="docState === 'saved'" key="saved">Edit</button>
  3. <button v-else-if="docState === 'edited'" key="edited">Save</button>
  4. <button v-else-if="docState === 'editing'" key="editing">Cancel</button>
  5. </transition>

可以重写为:

  1. <script setup lang="ts">
  2. import { ref, computed } from 'vue';
  3. const docState = ref('saved');
  4. const buttonMessage = computed(() => {
  5. switch (docState.value) {
  6. case 'saved':return 'Edit';
  7. case 'edited':return 'Save';
  8. case 'editing': return 'Cancel';
  9. }
  10. });
  11. </script>
  12. <template>
  13. <transition>
  14. <button :key="docState">{{buttonMessage}}</button>
  15. </transition>
  16. </template>

@过渡模式

<transition> 的默认行为 - 进入和离开同时发生,即 上一个组件还在消失的过程中,但下一个组件已经在出现过程中。我们看看一组示例:

  1. <script setup lang="ts">
  2. import { ref, computed } from 'vue';
  3. // -- 定义 buttonState 形状(TS语法)
  4. type ButtonStateType = 'disable' | 'enable';
  5. // -- 定义 buttonState 变量,其类型为 ButtonStateType
  6. const buttonState = ref<ButtonStateType>('disable');
  7. </script>
  8. <template>
  9. <transition>
  10. <button type="button" v-if="buttonState === 'enable'" @click="buttonState = 'disable'">禁用</button>
  11. <button type="button" v-else @click="buttonState = 'enable'">启用</button>
  12. </transition>
  13. </template>
  14. <style scoped>
  15. @keyframes move-in {
  16. from {
  17. transform: translateX(100px);
  18. opacity: 0;
  19. }
  20. to {
  21. transform: translateX(0);
  22. opacity: 1;
  23. }
  24. }
  25. button {
  26. /* 为了方便查看效果,使用绝对定位使其重叠在一起 */
  27. position: absolute;
  28. }
  29. .v-enter-active {
  30. animation: move-in 1s linear;
  31. }
  32. .v-leave-active {
  33. animation: move-in 1s linear reverse;
  34. }
  35. </style>

示例效果:

trans_mode.gif

可以看到,在多组件切换时,进入和离开是同时发生的。同时生效的进入和离开的过渡不能满足所有要求,所以 Vue 提供了 过渡模式

  • in-out:新元素先进行进入过渡,完成之后当前元素过渡离开。
  • out-in:当前元素先进行离开过渡,完成之后新元素过渡进入。

语法形式如下:

  1. <transition mode="in-out">
  2. <!-- ... the buttons ... -->
  3. </transition>

接下来,我们切换两种模式查看效果:

**in-out**

trans_mode_in_out.gif

**out-in**

trans_mode_out_in.gif

不难发现,in-outout-in 模式刚好相反。

四、列表过渡

目前为止,关于过渡我们已经讲到:

  • 单个节点
  • 多个节点,每次只渲染一个

那么怎么同时渲染整个列表,比如使用 v-for?在这种场景下,我们会使用 <transition-group> 组件。在我们深入例子之前,先了解关于这个组件的几个特点:

  • 默认情况下,它不会渲染一个包裹元素,但是你可以通过 tag 属性 指定渲染一个元素。
  • 过渡模式不可用,因为我们不再相互切换特有的元素。
  • 内部元素 总是需要 提供唯一的 key 属性值。
  • CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。

1. 列表的进入& 离开过渡

现在让我们由一个简单的例子深入,进入和离开的过渡使用之前一样的 CSS 类名。

  1. <script setup lang="ts">
  2. import { reactive } from 'vue';
  3. const state = reactive({
  4. list: [1, 2, 3, 4, 5, 6],
  5. nextNum: 7,
  6. });
  7. // methods
  8. const randomIndex = () => Math.floor(Math.random() * state.list.length);
  9. // events
  10. const onInsert = () => {
  11. state.list.splice(randomIndex(), 0, ++state.nextNum);
  12. };
  13. const onRemove = () => {
  14. state.list.splice(randomIndex(), 1);
  15. };
  16. </script>
  17. <template>
  18. <!-- 按钮 -->
  19. <button type="button" @click="onInsert">INSERT</button>
  20. <button type="button" @click="onRemove">REMOVE</button>
  21. <!-- 列表渲染 -->
  22. <transition-group name="list" tag="div" class="list">
  23. <div class="item" v-for="item in state.list" :key="item">
  24. {{ item }}
  25. </div>
  26. </transition-group>
  27. </template>
  28. <style scoped>
  29. button {
  30. margin-right: 10px;
  31. margin-bottom: 16px;
  32. cursor: pointer;
  33. }
  34. .item {
  35. display: inline-block;
  36. margin-right: 10px;
  37. }
  38. .list-enter-active,
  39. .list-leave-active {
  40. transition: all 1s ease;
  41. }
  42. .list-enter-from,
  43. .list-leave-to {
  44. opacity: 0;
  45. transform: translateY(30px);
  46. }
  47. </style>

示例效果:

trans_list_1.gif

这个例子有一个问题,当添加和移除元素的时候,周围的元素会 瞬间移动 到它们的新布局的位置,而不是平滑的过渡,我们下面会解决这个问题。

2. 列表的移动过渡

为了解决上述示例在添加元素时瞬间移动的问题,可以使用新增的 **v-move** 类,它会应用在元素改变定位的过程中。像之前的类名一样,它的前缀可以通过 name 属性来自定义,也可以通过 move-class 属性手动设置。

v-move 对于设置过渡的切换时机和过渡曲线非常有用,继续上述的例子,我们通过 Lodash >> 打乱集合顺序。

首先安装 loadash:

  1. $ npm install lodash
  2. $ npm install @types/lodash --save-dev

修改示例代码:

  1. <script setup lang="ts">
  2. // +++
  3. import _ from 'lodash';
  4. // +++
  5. import { reactive } from 'vue';
  6. const state = reactive({
  7. list: [1, 2, 3, 4, 5, 6],
  8. nextNum: 7
  9. });
  10. // methods
  11. const randomIndex = () => Math.floor(Math.random() * state.list.length);
  12. // events
  13. // +++
  14. const onShuffle = () => {
  15. // 打乱集合顺序
  16. state.list = _.shuffle(state.list);
  17. };
  18. // +++
  19. const onInsert = () => {
  20. state.list.splice(randomIndex(), 0, ++state.nextNum);
  21. };
  22. const onRemove = () => {
  23. state.list.splice(randomIndex(), 1);
  24. };
  25. </script>
  26. <template>
  27. <!-- +++ -->
  28. <button type="button" @click="onShuffle">SHUFFLE</button>
  29. <!-- +++ -->
  30. <button type="button" @click="onInsert">INSERT</button>
  31. <button type="button" @click="onRemove">REMOVE</button>
  32. <transition-group name="list" tag="div" class="list">
  33. <div class="item" v-for="item in state.list" :key="item">
  34. {{ item }}
  35. </div>
  36. </transition-group>
  37. </template>
  38. <style scoped>
  39. button {
  40. margin-right: 10px;
  41. margin-bottom: 16px;
  42. cursor: pointer;
  43. }
  44. .item {
  45. display: inline-block;
  46. margin-right: 10px;
  47. }
  48. /* +++ */
  49. .list-move {
  50. transition: transform 1s;
  51. }
  52. /* +++ */
  53. .list-enter-active,
  54. .list-leave-active {
  55. transition: all 1s ease;
  56. }
  57. .list-enter-from,
  58. .list-leave-to {
  59. opacity: 0;
  60. transform: translateY(30px);
  61. }
  62. </style>

提示:代码中的 +++ 表示新增代码。

示例效果:

list-move.gif

这个看起来很神奇,其实 Vue 内部使用了一个叫 FLIP 的动画技术,它使用 transform 将元素从之前的位置平滑过渡到新的位置。

提示:需要注意的是使用 FLIP 过渡的元素不能设置为 display: inline。作为替代方案,可以设置为 display: inline-block 或者将元素放置于 flex 布局中。

3. 列表的交错过渡

通过 data 属性与 JavaScript 通信,就可以实现列表的交错过渡:

  1. <script setup lang="ts">
  2. import { reactive } from 'vue';
  3. import gsap from 'gsap';
  4. interface StateProps {
  5. list: number[] | null;
  6. }
  7. const state = reactive<StateProps>({
  8. list: null,
  9. });
  10. // -- 模拟请求数据
  11. setTimeout(() => {
  12. state.list = [1, 2, 3, 4, 5];
  13. }, 1000);
  14. const beforeEnter = (el: Element) => {
  15. const dom = el as HTMLDivElement;
  16. dom.style.cssText = 'opacity: 0; transform: translateY(30px)';
  17. };
  18. const enter = (el: Element, done: () => void) => {
  19. const dom = el as HTMLDivElement;
  20. const dataset = dom.dataset;
  21. const index = dataset.index || ''; /** 获取data-index,用于设置延迟以达到列表交错效果 */
  22. gsap.to(dom, {
  23. duration: 1,
  24. opacity: 1,
  25. translateY: 0,
  26. delay: +index * 0.25,
  27. onComplete: done,
  28. });
  29. };
  30. </script>
  31. <template>
  32. <transition-group
  33. tag="div"
  34. :css="false"
  35. @before-enter="beforeEnter"
  36. @enter="enter"
  37. >
  38. <div
  39. class="item"
  40. v-for="(item, index) in state.list"
  41. :key="item"
  42. :data-index="index"
  43. >
  44. <div class="avatar"></div>
  45. <div class="info">
  46. <div class="title"></div>
  47. <div class="desc"></div>
  48. </div>
  49. </div>
  50. </transition-group>
  51. </template>
  52. <style scoped>
  53. .item {
  54. width: 90%;
  55. padding: 10px;
  56. border-radius: 6px;
  57. box-shadow: 0 0 10px 1px #eeeeee;
  58. margin: 0 auto 16px;
  59. display: flex;
  60. align-items: center;
  61. }
  62. .avatar {
  63. width: 60px;
  64. height: 60px;
  65. background: #6bb6fc;
  66. border-radius: 12px;
  67. margin-right: 16px;
  68. }
  69. .title {
  70. width: 160px;
  71. height: 20px;
  72. border-radius: 20px;
  73. background: #6bb6fc;
  74. margin-bottom: 10px;
  75. }
  76. .desc {
  77. width: 80px;
  78. height: 20px;
  79. border-radius: 20px;
  80. background: #9ed0f8;
  81. }
  82. </style>

演示效果:

list-stagger.gif

五、状态过渡

Vue 的过渡系统提供了非常多简单的方法来设置进入、离开和列表的动效,那么对于数据元素本身的动效呢?比如:

  • 数字和运算
  • 颜色的显示
  • SVG 节点的位置
  • 元素的大小和其他的属性

这些数据要么本身就以数值形式存储,要么可以转换为数值。有了这些数值后,我们就可以结合 Vue 的响应性和组件系统,使用 第三方库 来实现切换元素的过渡状态。

@GSAP

GSAP是 GreenSock 提供的一个制作动画的 JavaScript 库:

接下来,我们通过 GSAP 结合 Vue 实现数字滚动的效果。

首先,安装 gsap:

  1. $ npm install gsap

然后直接上示例代码:

  1. <script setup lang="ts">
  2. import { reactive } from 'vue';
  3. import gsap from 'gsap';
  4. const state = reactive({
  5. count: 100,
  6. });
  7. const onPlus = () => {
  8. gsap.to(state, {
  9. duration: 0.75 /** 持续时间 */,
  10. count: state.count + Math.random() * 100 /** 变更key-value */,
  11. ease: 'sine' /** 速度曲线 */,
  12. });
  13. };
  14. </script>
  15. <template>
  16. <button type="button" style="cursor: pointer" @click="onPlus">增加数额</button>
  17. <p>&yen;&nbsp;{{ state.count.toFixed(2) }}</p>
  18. </template>

演示效果:

trans_gsap.gif