一、Transition

1、简介

Transition是Vue的一个内置组件,用于制作基于元素状态变化的动画效果,无需注册即可在任意组件实例中使用。该组件内部的默认插槽仅支持渲染单个元素或单根节点组件(内部也可以包裹多个节点,但任意时刻只能有一个节点被渲染),组件会将定义的插入和移除动画通过默认插槽传递给内部包裹的元素或组件上。

​ 当Transition组件内部的元素被插入和移除时,Vue会自动执行以下操作

  • 自动检测元素是否应用了CSS动画。如果应用了动画,则相关的class类名会在对应时机被添加和移除。
  • 检测元素是否设置了相关JavaScript钩子函数。如果是,则相关钩子函数会在对应时机被调用。
  • 如果以上两者都没有,则会在浏览器的下一个动画帧执行插入/移除操作。
元素的插入和移除的触发场景有以下几种:
  • v-if/v-else/v-else-if触发的切换。
  • v-show触发的切换。
  • component动态组件触发的切换。
  • 改变特殊的key属性。
2、动画效果
① 默认动画class类名

<Transition> 组件提供了6个应用于进入和离开过渡动画效果的class类名,这些类名会在适当的时机被添加到内部元素的根节点上,也会在适当的时机被移除。不同阶段类名样式之间的变化,共同组成了整体动画效果:

  • v-enter-from:设置进入动画起始状态的样式。该类名在元素进入前添加,在元素进入完成后的下一帧被移除。
  • v-enter-active:设置进入动画生效状态的样式。该类名应用于进入动画的整个阶段,在元素进入前添加,在动画完成后被移除。通常用来定义进入动画的持续时间、延迟与速度曲线等属性。
  • v-enter-to:设置进入动画结束状态的样式。该类名在元素进入完成后的下一帧添加,也就是v-enter-from被移除的那一帧,在动画完成之后被移除。
  • v-leave-from:设置离开动画起始状态的样式。该类名在离开动画被触发时立即添加,在下一帧后被移除。
  • v-leave-active:设置离开动画生效状态的样式。该类名应用于离开动画的整个阶段,在离开动画被触发时立即添加,在动画完成之后移除。通常用来定义离开动画的持续时间、延迟与速度曲线。
  • v-leave-to:设置离开动画结束状态的样式。该类名在离开动画被触发后的下一帧被添加,也就是v-leave-active被移除的那一帧,在动画完成之后被移除。

class类名作用区间:

在这里插入图片描述

② 动画class命名

​ 可以通过给<Transition>组件设置name属性,从而对当前过渡动画进行命名:

<Transition name="fade">
 。。。
</Transition>

​ 动画被命名后,对应的过渡动画class类名也会相应变化,不再以默认v-*作为前缀,而是以name-*作为前缀。以上面的name="fade"为例,具体的class类名将变为:

// 进入动画的类名
.fade-enter-from
.fade-enter-active
.fade-enter-to

// 离开动画的类名
.fade-leave-from
.fade-leave-active
.fade-leave-to
③ 自定义动画class

​ 如果不想使用Vue默认的动画class类名规则,可以通过<Transition>组件的props,来自定义动画class类名。传入的class类名会覆盖对应的默认class类名,通常在结合其他第三方CSS动画库实现动画效果时使用。

  • enter-form-class:自定义一个覆盖 v-enter-from的class类名。
  • enter-active-class:自定义一个覆盖v-enter-active的class类名。
  • enter-to-class:自定义一个覆盖 v-enter-to的class类名。
  • leave-from-class:自定义一个覆盖 v-leave-from的class类名。
  • leave-active-class:自定义一个覆盖 v-leave-active的class类名。
  • leave-to-class:自定义一个覆盖 v-enter-to的class类名。
<!-- 结合 Animate.css 实现动画效果 -->
<Transition
  name="custom-classes"
  enter-active-class="animate__animated animate__tada"
  leave-active-class="animate__animated animate__bounceOutRight"
>
  。。。
</Transition>
④ 结合 transition 设置动画效果

<Transition>组件想要实现丝滑的动画效果,最常用的方式是搭配CSS的transition过渡属性实现,这从组件名称上也有所提现。可以在不同阶段的class类名中设置不同的transition属性,从而精确的控制不同阶段的动画效果。

<Transition name="fade">
  <div class="test-div" v-show="show">
     测试命名Transition
  </div>
</Transition>

<style>
/* 其他代码....  */

/* 设置动画各个阶段 对应类名的样式 */
.fade-enter-from,
.fade-leave-to
 {
  opacity: 0;
}
/* 插入动画阶段的过渡效果 */
.fade-enter-active {
  transition: opacity 0.8s ease-out;
}
/* 移除动画阶段的过渡效果 */
.fade-leave-active {
  transition: opacity 1.6s ease-in;
}

.fade-enter-to,
.fade-leave-from {
  opacity: 1;
}
</style>
⑤ 结合 animation 设置动画效果

<Transition>组件想要实现丝滑的动画效果,还可以搭配CSS的animation动画属性实现。但要注意,此时的v-leave-from类名,不是在元素插入后被移除,而是等插入阶段animationanimationend事件被触发后才会被移除。

​ 大多数情况下,只需要在v-enter-activev-leave-active这两个类名下,使用相应的animation即可,无需使用其他类名配合。

<Transition name="bounce">
  <span v-show="show" style="display: inline-block;">
     bounce
  </span>
</Transition>

<style>
@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.25);
  }
  100% {
    transform: scale(1);
  }
}
/* 插入阶段的动画 */
.bounce-enter-active {
  animation: bounce-in 0.5s;
}
/* 移除阶段的动画 */
.bounce-leave-active {
  animation: bounce-in 0.5s reverse;
}
</style>
⑥ 同时使用 transition 和 animation

​ Vue内部是通过附加事件监听器(transitionendanimationend),知晓动画是何时结束的,从而添加和移除相应的class。如果代码中只使用了transitionanimation两者其中之一,则Vue可以自动探测到要附加的事件监听器。

​ 但如果在一个动画中同时使用两者,则需要在<Transition>组件上通过 type prop显式的指定Vue需要关注的事件类型,值可以是:animationtransition

<Transition type="animation">...</Transition>
<Transition type="transition">...</Transition>
⑦ 深层动画和设置动画时长

​ 动画class类名只会应用在<Transition>组件的直接子元素上,但是可以通过后代选择器的方式,在深层元素上设置相应动画效果。

.v-enter-active .inner,
.v-leave-active .inner {
  transition: all 0.3s ease-in-out;
}

.v-enter-from .inner,
.v-leave-to .inner {
  opacity: 0;
}
.v-enter-to .inner,
.v-leave-from .inner {
  opacity: 1;
}

​ 但这样会引发另一个问题:默认<Transition> 组件是通过监听直接子元素的transitionendanimationend事件,判断动画何时结束。但此时设置的是后代元素的过渡动画,无法被组件监听到。

​ 针对这个问题,只能通过在<Transition>组件上设置duration prop ,显式的指定动画的整体持续时间,从而让组件知晓动画何时结束。值为一个数字,单位为毫秒(ms)。

<Transition :duration="300">...</Transition>
3、JavaScript钩子函数

<Transition> 组件在动画执行的多个节点上设置了钩子函数,当动画执行到对应阶段节点时,对应的钩子函数会被调用。

​ 这些钩子函数可以与CSS动画结合使用,也可以与JS动画结合使用,在对应的钩子函数中执行对应的JS代码。如果仅与JS动画结合使用,最好是在<Transition> 组件上增加:css="false"的prop,使Vue自动跳过对CSS动画的相关监听,既能提升性能,又能防止CSS的干扰。但在 @enter@leave钩子函数中,必须根据时机自主调用done()回调函数,否则Vue会自动同步调用该回调函数,导致动画立即完成。

<Transition :css="false">
    ...
</Transition>
具体钩子函数如下:
<Transition name="fade"
   @before-enter="onBeforeEnter"
   @enter="onEnter"
   @after-enter="onAfterEnter"
   @enter-cancelled="onEnterCancelled"
   @before-leave="onBeforeLeave"
   @leave="onLeave"
   @after-leave="onAfterLeave"
   @leave-cancelled="onLeaveCancelled"
>
   <div class="test-div" v-show="show">
      测试Transition
    </div>
</Transition>

<script setup lang="ts">
// 在元素被插入DOM之前被调用 结合JS时可以用来设置v-enter-from的状态
const onBeforeEnter = (el: any) => {
  // 参数 el 表示被插入的元素DOM
  console.log('before-enter---',el);
}
// 在元素被插入DOM后的下一帧被调用 结合JS时可以用来开始插入动画
const onEnter = (el: any, done: any) => {
  // 参数 el 表示被插入的元素DOM
  console.log('enter---',el , done);
  // 参数 done 表示动画结束的回调函数 结合JS时需自主调用该函数
  // 如果是与CSS动画结合使用时 则该参数是可选参数 因为Vue会监听动画的结束事件
  done();
}
// 当插入动画结束时被调用 结合JS时可以做一些清理操作
const onAfterEnter = (el: any) => {
  // 参数 el 表示被插入的元素DOM
  console.log('after-enter---',el);
}
// 当插入动画在完成之前被取消时调用 结合JS时可以做一些清理操作
const onEnterCancelled = (el: any) => {
  // 参数 el 表示被插入的元素DOM
  console.log('enter-cancelled---',el);
}
// 当移除动画开始之前被调用 不常用 
const onBeforeLeave = (el: any) => {
  // 参数 el 表示被移除的元素DOM
  console.log('before-leave---',el);
}
// 当移除动画开始时被调用 结合JS时可以用来开始离开动画
const onLeave = (el: any, done: any) => {
  // 参数 el 表示被移除的元素DOM
  console.log('leave---',el , done);
  // 参数 done 表示动画结束的回调函数 结合JS时需自主调用该函数
  // 如果是与CSS动画结合使用时 则该参数是可选参数 因为Vue会监听动画的结束事件
  done();
}
// 当移除动画结束 且元素已经从DOM中移除时被调用 结合JS时可以做一些清理操作
const onAfterLeave = (el: any) => {
  // 参数 el 表示被移除的元素DOM
  console.log('after-leave---',el);
}
// 当移除动画在完成之前被取消时调用  结合JS时可以做一些清理操作
const onLeaveCancelled = (el: any) => {
  console.log('leave-cancelled---',el);
}
</script>
4、初次渲染应用动画

​ 默认情况下,<Transition> 组件只会在元素插入和移除时应用动画效果,并不包含初次渲染。如果希望在元素初次渲染时,也应用插入动画效果,需要组件上在设置appear prop:

<Transition appear>
  ...
</Transition>
5、动画模式

<Transition> 组件内部可以包裹多个节点,但需要通过v-if/v-show等条件判断,保证同一时刻只有一个节点被渲染。默认情况下,当判断条件变化,需要切换渲染的节点时,被移除节点的移除动画和要插入节点的插入动画时同时开始的,这很容易造成布局冲突,从而影响动画效果。

​ 虽然可以通过设置节点的position属性,来解决部分场景下的布局冲突问题,但还是有些局限。

​ 针对这个问题,组件为我们提供了mode prop,允许指定动画的执行模式,属性值可以被设置为out-inin-outmode="out-in" 表示先执行移除动画,等移除动画执行结束后,再执行插入动画; mode="out-in" 表示先执行插入动画,等插入动画执行结束后,再执行移除动画。

<Transition mode="out-in">
  ...
</Transition>
6、动态 Props

<Transition> 组件的所有 props 都可以绑定动态变量,从而能更好的掌控动画行为。 例如给name绑定动态变量,提前定义多组动画class,通过动态修改name属性的值,实现动态的切换动画效果。

<Transition :name="transitionName">
  <!-- ... -->
</Transition>

<script setup lang="ts">
import { ref } from 'vue'

const transitionName = ref('fade')

setTimeout(() => {
  transitionName.value = 'bounce'
}, 4000)
</script>
7、节点的Key 属性

​ 当<Transition> 组件内部只有DOM内部的文本节点发生更新时,由于DOM节点本身并不会被重新渲染,也就不会触发动画效果。如果想要在类似这种情况下,强制重新渲染DOM节点,触发动画效果,我们可以通过给DOM节点设置一个动态的key属性,随着内部文本节点的更新而更新。这样子,当key发生变化时,Vue会创建并渲染一个新的DOM节点,内部设置新的文本节点,同时移除旧的DOM节点,从而触发对应的动画效果。

<template>
  <Transition>
    <!-- 当key发生变化时 Vue会创建一个新的dom元素放在这个位置 从而触发动画效果 -->
    <span :key="count">{{ count }}</span>
  </Transition>
</template>

<script setup>
import { ref } from 'vue';
const count = ref(0);

setInterval(() => count.value++, 1000);
</script>

二、TransitionGroup

1、简介

TransitionGroup是Vue的一个内置组件,用于制作基于 v-for 列表中多个元素或组件的插入、移除和顺序改变的动画效果,无需注册即可在任意组件实例中使用。

TransitionGroupTransition功能基本相似,两者的props、动画class、JavaScript钩子函数等都基本相同,只有部分特性有所差异。

与Transition的区别:
  • Transition适用于实现单个元素或组件的动画,TransitionGroup适用于实现多个元素或组件的列表的动画。
  • Transition可以通过mode prop 指定动画模式,TransitionGroup不存在该prop。
  • Transition只有在想要强制重新渲染DOM时,才需要给内部元素设置key属性,而TransitionGroup要求内部所有列表元素都必须有一个独一无二的key 属性。
  • TransitionGroup新增了tagmoveClass两个prop,分别用于指定容器元素和移动期间的动画class。
2、容器元素

​ 默认情况下,<TransitionGroup>组件并不会在内部列表元素的外层渲染一个容器元素。但在某些情况下,我们希望有一个容器元素,那么可以通过tag prop 指定一个元素类型,然后Vue会根据这个类型去渲染一个对应的元素作为容器元素。

​ 这个容器元素并不会影响动画效果,因为动画的class依旧是应用在列表元素上,而非容器元素。

<!-- tag指定一个ul作为容器元素 -->
<TransitionGroup name="list" tag="ul">
  <li v-for="item in items" :key="item">
    ...
  </li>
</TransitionGroup>

<!-- 渲染后的DOM结构 -->
<ul>
  <li>
   ...
  </li>
</ul>
3、移动动画class

​ 针对列表元素被添加或移除时,其周边元素发生”跳跃“而非平滑过渡的现象,TransitionGroup新增了一个动画class:v-move。该class应用于元素位置发生变化的过程中,也就是移动中的元素,如插入/移除列表元素时,其周边元素的位置变化过程。

​ 当然,该class默认也会受到name属性的影响,变为name-move。除此之外,还可以通过moveClass prop 显式的指定移动动画class。

<div class="btn" @click="add()">添加列表元素</div>
<div class="btn" @click="remove()">移除列表元素</div>
<!-- tag指定一个ul作为容器元素 -->
<!-- name指定动画名称 -->
<TransitionGroup name="list" tag="ul">
  <li v-for="item in items" :key="item">
    {{ item }}
  </li>
</TransitionGroup>

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

const items = ref([1,2,3,4,5])
const add = () => {
  items.value = [...items.value.slice(0,3), 666, ...items.value.slice(3)]
}

const remove = () => {
  items.value.splice(3, 1)
}
</script>

<style lang="scss" scoped>
/* 1. 声明动画过渡效果 */
/* 包括移动、插入和移除 */
.list-move,
.list-enter-active,
.list-leave-active {
  transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
}

/* 2. 声明插入前和移除后的状态 */
.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: scaleY(0.01) translate(30px, 0);
}

/* 3. 将要移除的项目设置定位 脱离普通布局流 */
/* 以便正确地计算移动时的动画效果 */
.list-leave-active {
  position: absolute;
}
</style>

请关注公众号,查看更多优质资源:

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐