开发过渡动效的利器,轻松实现动画过渡效果!

1,175 阅读19分钟

Transition 组件是 Vue 提供的内置组件,为单个元素或组件提供动画过渡效果。所谓过渡效果,其实就是从一个状态变化到另一个状态的过程,比如:从隐藏到显示、从进入到离开等。

Transition 组件会在一个元素或组件进入和离开 DOM 时应用动画。

进入或离开可由以下的条件之一触发,这也是 Transition 组件进入或离开动画生效的条件:

  • v-if 所触发的切换

  • v-show 所触发的切换

  • 由特殊元素 <component> 切换的动态组件

  • 改变元素或组件的 key 属性

例如下面这个最基本的例子。这个例子实现了通过点击 Toggle 按钮,p 标签会在 0.5s 的时间里进行显示或隐藏的切换:

<button @click="show = !show">Toggle</button>
<Transition>
  <p v-if="show">hello</p>
</Transition>
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

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

当一个 Transition 组件中的元素被插入或移除时,会发生下面这些事情:

  1. Vue 会自动检测目标元素是否应用了 CSS 过渡或动画。如果是,则一些 CSS 过渡 class 会在适当的时机被添加和移除。

  2. 如果有作为监听器的 JavaScript 钩子,这些钩子函数会在适当时机被调用。

  3. 如果没有探测到 CSS 过渡或动画、也没有提供 JavaScript 钩子,那么 DOM 的插入、删除操作将在浏览器的下一个动画帧后执行。

Transition 组件支持使用 CSS 实现过渡效果,也支持使用 JavaScript 实现过渡效果,当然啦,Transition 是个灵活的组件,CSS 和 JavaScript 也可以结合使用,实现过渡效果。

总体来说,Transition 组件通过结合适当的 CSS 或 JavaScript 可以实现各种不同的进入或离开的过渡效果。

基于 CSS 的过渡效果

CSS 过渡 class

Transition 组件提供了 6 个 CSS class 应用于进入与离开过渡效果中。这 6 个 CSS class 分别代码了进入与离开过渡效果中 6 个不同的状态。

这 6 个 CSS class 分别为 v-enter-fromv-enter-activev-enter-tov-leave-fromv-leave-activev-leave-to

pic16.png

v-enter-fromv-enter-activev-enter-to 分别代表进入动画的起始状态、进入动画的生效状态和进入动画的结束状态。

v-leave-fromv-leave-activev-leave-to 分别代表离开动画的起始状态、离开动画的生效状态和离开动画的结束状态。

v-enter-from 会在元素插入之前添加,在元素插入完成后的下一帧移除。

v-enter-active 会应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。v-enter-active 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。

v-enter-to 代表进入动画的最终状态,在元素插入完成后的下一帧被添加(也就是 v-enter-from 被移除的同时),在过渡或动画完成之后移除。

v-leave-from 在离开过渡效果被触发时立即添加,在一帧后被移除。

v-leave-active 会应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。v-leave-active 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。

v-leave-to 代表离开动画的最终状态,在一个离开动画被触发后的下一帧被添加 (也就是 v-leave-from 被移除的同时),在过渡或动画完成之后移除。

v-enter-activev-leave-active 给我们提供了为进入和离开动画指定不同速度曲线的能力。

为过渡效果命名

Transition 组件提供的 6 个 class 名都是 v 开头的,我们可以给 Transition 组件传一个 name prop 声明一个过渡效果名。这样那 6 个 class 名字不会用 v 作前缀,而是以传入的 name 为开头。

比如,我们给 Transition 组件传入的 name prop 为 fade:

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

对于一个有名字的过渡效果,对他起作用的过渡 class 不会以 v 作为前缀,而是以 fade 为前缀:

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

CSS 的 transition 与 CSS 的 animation

Transition 组件通常会搭配原生 CSS 过渡原生 CSS 动画一起使用。

我们可以使用原生 transition CSS 属性定义需要执行动画的属性、持续时间和速度曲线

例如下面的例子,借助原生 transition CSS 属性,在进入和离开动画中定义不同持续时间和速度曲线。

<Transition name="slide-fade">
  <p v-if="show">hello</p>
</Transition>
/*
  进入和离开动画可以使用不同
  持续时间和速度曲线。
*/
.slide-fade-enter-active {
  transition: all 0.3s ease-out;
}

.slide-fade-leave-active {
  transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
}

.slide-fade-enter-from,
.slide-fade-leave-to {
  transform: translateX(20px);
  opacity: 0;
}

原生 CSS 动画和 CSS transition 的应用方式基本上是相同的,只有一点不同,那就是 *-enter-from 不是在元素插入后立即移除,而是在一个 animationend 事件触发时被移除。

animationend 事件在 CSS 动画完成时触发。Element: animationend event

对于大多数的 CSS 动画,只需要简单地在 *-enter-active*-leave-active class 下声明他们,那 CSS 动画就会在过渡过程中生效。比如下面的例子:

<Transition name="bounce">
  <p v-if="show" style="text-align: center;">
    Hello here is some bouncy text!
  </p>
</Transition>
.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);
  }
}

自定义过渡 class

我们也可以向 Transition 组件传递以下 props 来指定自定义的过渡 class :

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

我们传入的这些 class 会覆盖相应阶段的默认 class 名,这在需要在 Vue 的动画机制下集成其他的第三方 CSS 动画库时非常有用,比如 Animate.css

<!-- 假设你已经在页面中引入了 Animate.css -->
<Transition
  name="custom-classes"
  enter-active-class="animate__animated animate__tada"
  leave-active-class="animate__animated animate__bounceOutRight"
>
  <p v-if="show">hello</p>
</Transition>

同时使用 transition 和 animation

Transition 组件支持同时使用 transition 和 animation ,如果 transition 和 animation 同时使用的话,需要用户显示地传入 type prop 来声明,告诉 Vue 需要关心哪种类型,传入的值是 animationtransition

如果传入的 typeanimation ,则 Vue 内部会通过监听 animationend 得知过渡已结束。

如果传入的 typetransition ,则 Vue 内部会通过监听 transitionend 得知过渡已结束。

pic17.png

👆 图中代码源自 Vue.js 3.2.45 版本

type prop 是传 animation 还是 transition 取决于你所应用的 CSS 规则。

如下面的例子,这个例子同时使用了 原生的 transition CSS 规则和 animation CSS 规则:

<script src="../../dist/vue.global.js"></script>

<style>
.box {
  width: 200px;
  height: 100px;
  background-color: #42b983;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Fade transition */
.fade-enter-active, .fade-leave-active {
  /* 同时使用了 transition 和 animation */
  transition: opacity 1s ease;
  animation: slide-in 10s ease forwards; /* Animation */
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* Keyframes for animation */
@keyframes slide-in {
  from {
    transform: translateY(-50px);
  }
  to {
    transform: translateY(0);
  }
}
</style>

<div id="demo">
  <button @click="toggle">Toggle</button>
  <!-- type 为 animation -->
  <transition name="fade" type="animation">
    <div v-if="show" class="box">Hello, Vue!</div>
  </transition>
</div>

<script>
const { createApp, ref } = Vue

createApp({
  setup() {
    const show = ref(false)

    const toggle = () => {
      show.value = !show.value
    }
    return {
      show,
      toggle
    }
  }
}).mount('#demo')
</script>

传入的 typeanimation ,则过渡会在 10s 后结束。如果传入的 typetransition ,则过渡会在 1s 后结束。

如果仅仅使用 animationtransition 二者的其中之一,Vue 可以自动探测到正确的类型。具体原理是通过 window.getComputedStyle 方法获得 dom 上的样式规则,然后通过获得的样式规则比较 transitionanimation 各自的持续时间得出。如果 transition 持续的时间长,则 typetransition 类型。如果 animation 的持续的时间长,则 typeanimation 类型。

pic18.png

pic19.png

官方的 API 文档对 type prop 的作用也总结得非常好:

pic20.png

深层级过渡与显式过渡时长

尽管 Transition 组件内部提供的过渡 class 仅能应用在 Transition 组件的直接子元素上,我们还是可以使用深层级的 CSS 选择器,在深层级的元素上触发过渡效果:

<Transition name="nested">
  <div v-if="show" class="outer">
    <div class="inner">
      Hello
    </div>
  </div>
</Transition>
/* 应用于嵌套元素的规则 */
.nested-enter-active .inner,
.nested-leave-active .inner {
  transition: all 0.3s ease-in-out;
}

.nested-enter-from .inner,
.nested-leave-to .inner {
  transform: translateX(30px);
  opacity: 0;
}

/* ... 省略了其他必要的 CSS */

我们还可以在深层元素上添加一个过渡延迟,从而创建一个带渐进延迟的动画序列:

/* 延迟嵌套元素的进入以获得交错效果 */
.nested-enter-active .inner {
  transition-delay: 0.25s;
}

但是,这会带来一个小问题。默认情况下,Transition 组件会通过监听过渡根元素上的第一个 transitionend 或者 animationend 事件来尝试自动判断过渡何时结束。也就是说 Vue 是无法自动监测到嵌套的子元素的过渡是何时结束的。而在嵌套的过渡中,期望的行为应该是等待所有内部元素的过渡完成。

这种情况下,Transition 组件为我们提供了 duration prop 来显示指定过渡的持续时间(以毫秒为单位)。我们可以计算出过渡的总持续时间,即延迟时间加外部和嵌套元素的过渡持续时间,传入 duration prop 中。明确告知 Transition 组件总的过渡持续时间。

<Transition :duration="550">...</Transition>

完整的例子如下:

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

const show = ref(true)
</script>

<template>
  <button @click="show = !show">Toggle</button>
  <Transition :duration="550" name="nested">
    <div v-if="show" class="outer">
      <div class="inner">
        Hello
      </div>
    </div>
  </Transition>
</template>

<style>
.outer, .inner {
  background: #eee;
  padding: 30px;
  min-height: 100px;
}
  
.inner { 
  background: #ccc;
}

/* 定义根元素进入动画和离开动画生效状态的过渡动效 */
.nested-enter-active, .nested-leave-active {
  transition: all 0.3s ease-in-out;
}

/* 根元素离开动画延迟 0.25 秒生效 */
.nested-leave-active {
  transition-delay: 0.25s;
}

/* 定义根元素进入动画起始状态和离开动画结束状态 */
.nested-enter-from,
.nested-leave-to {
  transform: translateY(30px);
  opacity: 0;
}


/* 定义嵌套元素进入动画生效状态和离开动画生效状态的过渡动效 */
.nested-enter-active .inner,
.nested-leave-active .inner { 
  transition: all 0.3s ease-in-out;
}
/* 嵌套元素进入动画延迟 0.25s 执行 */
.nested-enter-active .inner {
  transition-delay: 0.25s;
}

/* 定义嵌套元素进入动画起始状态和离开动画结束状态 */
.nested-enter-from .inner,
.nested-leave-to .inner {
  transform: translateX(30px);
  /*
    Hack around a Chrome 96 bug in handling nested opacity transitions.
    This is not needed in other browsers or Chrome 99+ where the bug
    has been fixed.
  */
  opacity: 0.001;
}
</style>

如果有必要,Transition 组件的 duration prop 还支持以对象的形式传入,分别指定进入和离开所需的时间:

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

性能考量

出于性能考量,我们要多使用 transformopacity 之类的 CSS 属性来制作动画。因为使用这些属性制作动画非常高效:

  1. 他们在动画过程中不会影响到 DOM 结构,因此不会每一帧都触发昂贵的 CSS 布局重新计算。

  2. 大多数的现代浏览器都可以在执行 transform 动画时利用 GPU 进行硬件加速。

相比之下,像 height 或者 margin 这样的属性会触发 CSS 布局变动,因此执行它们的动画效果更昂贵,需谨慎使用。

JavaScript 钩子

Transition 组件除了提供 6 个 class 定义过渡动效过渡时不同的状态外,还提供了 11 个 JavaScript 钩子在不同的过渡时机中执行:

  • @before-enter:在元素被插入到 DOM 之前被调用,用这个来设置元素的 "enter-from" 状态(进入动画的起始状态)

  • @before-leave:在 leave 钩子之前调用,即在离开过渡开始前调用,大多数时候,应该只会用到 leave 钩子。

  • @enter:在元素被插入到 DOM 之后的下一帧被调用,用这个来开始进入动画。

  • @leave:在离开过渡开始时调用,用这个来开始离开动画。

  • @appear:如果传入了 appear 为 true 的 prop ,则在过渡元素初次渲染时被调用。 注意需要过渡元素默认就在页面上才会调用此钩子。如果过渡元素一开始没有在页面上,经历了过渡才出现在页面上,则此钩子函数不会被调用。

  • @after-enter:当进入过渡完成时调用。

  • @after-leave:在离开过渡完成、且元素已从 DOM 中移除时调用。

  • @after-appear:也是与 appear 相关的钩子,当 appear prop 传入为 true ,且过渡元素默认在页面上,则此钩子在首次进入动画完成后调用。

  • @enter-cancelled:当元素的进入过渡被中断或取消时调用。例如,在进入过渡过程中,元素被移除或条件变更导致过渡未完成。

  • @leave-cancelledv-show only):当元素离开过渡被取消时会调用。注意有个前提,就是这个过渡是由 v-show 触发的。

  • @appear-cancelled:也是与 appear 相关的钩子,当 appear prop 传入为 true ,且过渡元素默认在页面上,则此钩子在首次进入动画取消后调用。

@enter@leave 钩子支持 done 回调函数。当传入 :css="false" 时,则完全由 JavaScript 全权控制过渡什么时候结束,这个时候 @enter@leave 钩子的回调函数 done 就是必须的。 只有执行了 @enter@leave 钩子的 done 回调函数,才算是真正完成了进入过渡动画或离开过渡动画。

@appear@after-appear 钩子的简单示例

<script src="../../dist/vue.global.js"></script>

<style>
.fade-enter-active, .fade-leave-active {
  transition: opacity 1s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

<div id="demo">
  <button @click="toggle">Toggle</button>
  <transition
    name="fade"
    appear
    @appear="onAppear"
    @after-appear="onAfterAppear"
  >
    <div v-if="show">Hello, Vue Transition!</div>
  </transition>
</div>

<script>
const { createApp, ref } = Vue

createApp({
  setup() {
    // 注意,show 初始值为 true ,
    // @appear 和 @after-appear 钩子才会被调用
    const show = ref(true)

    const toggle = () => {
      show.value = !show.value
    }
    const onAppear = () => {
      console.log('onAppear ==  ')
    }
    const onAfterAppear = () => {
      console.log('onAfterAppear == ')
    }
    
    return {
      show,
      toggle,
      onAppear,
      onAfterAppear
    }
  }
}).mount('#demo')
</script>

@enter-cancelled 钩子的简单示例

<script src="../../dist/vue.global.js"></script>

<style>
.fade-enter-active, .fade-leave-active {
  transition: opacity 1s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

<div id="demo">
  <button @click="toggle">Toggle</button>
  <transition
    name="fade"
    @enter-cancelled="onEnterCancelled"
  >
    <div v-if="show">Hello, Vue Transition!</div>
  </transition>
</div>

<script>
const { createApp, ref } = Vue

createApp({
  setup() {
    const show = ref(false)

    const toggle = () => {
      show.value = !show.value
      if (show.value) {
        setTimeout(() => {
          show.value = false // 立即取消进入过渡
        }, 100) // 100ms 后取消
      }
    }
    const onEnterCancelled = (el) => {
      // 处理进入过渡被取消的逻辑
      console.log('Enter transition was cancelled:', el)
    }
    
    return {
      show,
      toggle,
      onEnterCancelled,
    }
  }
}).mount('#demo')
</script>

@leave-cancelled 钩子的简单示例

<script src="../../dist/vue.global.js"></script>

<style>
.fade-enter-active, .fade-leave-active {
  transition: opacity 1s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

<div id="demo">
  <button @click="toggle">Toggle</button>
  <transition
    name="fade"
    @leave-cancelled="onLeaveCancelled"
  >
    <div v-show="show">Hello, Vue Transition!</div>
  </transition>
</div>

<script>
const { createApp, ref } = Vue

createApp({
  setup() {
    const show = ref(false)

    const toggle = () => {
      show.value = !show.value
      if (show.value === false) {
        setTimeout(() => {
          show.value = true // 立即取消离开过渡
        }, 800) // 800ms 后取消
      }
    }
    const onLeaveCancelled = (el) => {
      // 处理离开过渡被取消的逻辑
      console.log('Leave transition was cancelled:', el)
    }
    
    return {
      show,
      toggle,
      onLeaveCancelled,
    }
  }
}).mount('#demo')
</script>

@appear-cancelled 钩子的简单示例

<script src="../../dist/vue.global.js"></script>

<style>
.fade-enter-active, .fade-leave-active {
  transition: opacity 1s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

<div id="demo">
  <transition
    name="fade"
    appear
    @appear-cancelled="onAppearCancelled"
  >
    <div v-if="show">Hello, Vue Transition!</div>
  </transition>
</div>

<script>
const { createApp, ref, onMounted } = Vue

createApp({
  setup() {
    const show = ref(true)

    const toggle = () => {
      if (show.value) {
        setTimeout(() => {
          show.value = false // 立即取消 appear
        }, 800) // 800ms 后取消
      }
    }
    const onAppearCancelled = (el) => {
      // appear 被取消的逻辑
      console.log('appear transition was cancelled:', el)
    }

    onMounted(() => {
      toggle()
    })
    
    return {
      show,
      toggle,
      onAppearCancelled,
    }
  }
}).mount('#demo')
</script>

上面提到的 JavaScript 钩子可以和 CSS 过渡或动画结合使用,也可以单独使用。

在使用仅由 JavaScript 执行的动画时,建议添加一个 :css="false" prop。这显式地向 Vue 表明可以跳过对 CSS 过渡的自动探测。除了性能稍好一些之外,还可以防止 CSS 规则意外地干扰过渡效果:

<Transition
  ...
  :css="false"
>
  ...
</Transition>

纯粹使用 JavaScript 钩子实现过渡动画的例子

相对于使用 CSS 实现过渡动画,使用纯 JavaScript 实现过渡动画会麻烦一些。

例子1

<script src="../../dist/vue.global.js"></script>

<div id="demo">
  <button @click="toggle">Toggle</button>
  <transition
    @before-enter="onBeforeEnter"
    @enter="onEnter"
    @leave="onLeave"
    :css="false"
  >
    <div v-if="show">Hello, Vue Transition!</div>
  </transition>
</div>

<script>
const { createApp, ref } = Vue

createApp({
  setup() {
    const show = ref(false)

    const toggle = () => {
      show.value = !show.value
    }

    const onBeforeEnter = (el) => {
      // 在进入之前设置初始样式
      el.style.opacity = 0
    }

    const onEnter = (el, done) => {
      // 进入时的过渡效果
      el.offsetHeight; // 强制重排,才能使过渡生效
      el.style.transition = `opacity 3s`
      el.style.opacity = 1;
      done() // 过渡完成后调用 done      
    }

    const onLeave = (el, done) => {
      // 离开时的过渡效果
      el.style.transition = `opacity 3s`
      el.style.opacity = 0;
      // 过渡完成后调用 done
      setTimeout(() => {
        done()
      }, 3000) // 与过渡时间一致 
    }
    
    return {
      show,
      toggle,
      onBeforeEnter,
      onEnter,
      onLeave,
    }
  }
}).mount('#demo')
</script>

没有调用 @enter@leave 钩子的 done 回调函数,在完成离开动画后,过渡元素未真正移除,这是不符合期望的:

pic21.png

如果调用了 @enter@leave 钩子的 done 回调函数,在完成离开动画后,过渡元素会被移除,这才是符合期望的:

pic22.png

例子2

<script src="../../dist/vue.global.js"></script>

<div id="demo">
  <button @click="toggle">Toggle</button>
  <transition
    @enter="onEnter"
    @leave="onLeave"
    :css="false"
  >
    <div v-if="show">Hello, Vue Transition!</div>
  </transition>
</div>

<script>
const { createApp, ref } = Vue

createApp({
  setup() {
    const show = ref(false)

    const toggle = () => {
      show.value = !show.value
    }

    const onEnter = (el, done) => {
      // 进入过渡开始时的操作
      el.style.opacity = 0
      // 使用 requestAnimationFrame 确保样式应用后再进行过渡
      requestAnimationFrame(() => {
        el.style.transition = 'opacity 3s';
        el.style.opacity = 1;
        done(); // 过渡完成
      });      
    }
    const onLeave = (el, done) => {
      // 离开过渡开始时的操作
      el.style.transition = 'opacity 3s'
      el.style.opacity = 0;
      // 过渡完成后调用 done
      setTimeout(() => {
        done();
      }, 3000); // 与过渡时间一致
    }
    
    return {
      show,
      toggle,
      onEnter,
      onLeave,
    }
  }
}).mount('#demo')
</script>

可复用过渡效果

得益于 Vue 的组件系统,过渡效果可以被封装复用。要创建一个可被复用的过渡,只需要为 Transition 组件创建一个包装组件,并向内传入插槽内容:

<!-- MyTransition.vue -->
<script>
// JavaScript 钩子逻辑...
</script>

<template>
  <!-- 包装内置的 Transition 组件 -->
  <Transition
    name="my-transition"
    @enter="onEnter"
    @leave="onLeave">
    <slot></slot> <!-- 向内传递插槽内容 -->
  </Transition>
</template>

<style>
/*
  必要的 CSS...
  注意:避免在这里使用 <style scoped>
  因为那不会应用到插槽内容上
*/
</style>

现在 MyTransition 可以在导入后像内置组件那样使用了:

<MyTransition>
  <div v-if="show">Hello</div>
</MyTransition>

出现时过渡

如果想在某个节点初次渲染时应用一个过渡效果,可以添加 appear prop:

<Transition appear>
  ...
</Transition>

元素间过渡

除了通过 v-if / v-show 切换一个元素,我们也可以通过 v-if / v-else / v-else-if 在几个元素间进行切换,只要确保任一时刻只会有一个元素被渲染即可:

<Transition>
  <button v-if="docState === 'saved'">Edit</button>
  <button v-else-if="docState === 'edited'">Save</button>
  <button v-else-if="docState === 'editing'">Cancel</button>
</Transition>

完整的例子如下:

<script src="../../../dist/vue.global.js"></script>

<style>
.btn-container {
  display: inline-block;
  position: relative;
  height: 1em;
}

button {
  position: absolute;
}

/* 定义进入动画生效状态和离开动画生效状态过渡动效 */
.slide-up-enter-active,
.slide-up-leave-active {
  transition: all 0.25s ease-out;
}

/* 定义进入动画起始状态 */
.slide-up-enter-from {
  opacity: 0;
  transform: translateY(30px);
}

/* 定义离开动画结束状态 */
.slide-up-leave-to {
  opacity: 0;
  transform: translateY(-30px);
}
</style>

<div id="demo">
  <span style="margin-right: 20px">
    Click to cycle through states:
  </span>
  <div class="btn-container">
    <transition
      name="slide-up"
    >
      <button
        v-if="docState === 'saved'"
        @click="docState = 'edited'"
      >Edit</button>
      <button
        v-else-if="docState === 'edited'"
        @click="docState = 'editing'"
      >Save</button>
      <button
        v-else-if="docState === 'editing'"
        @click="docState = 'saved'"
      >Cancel</button>
    </transition>
  </div>
</div>

<script>
const { createApp, ref } = Vue

createApp({
  setup() {
    const docState = ref('saved')    
    return {
      docState,
    }
  }
}).mount('#demo')
</script>

过渡模式

上面的例子,进入和离开的元素都是在同时开始动画的,因此我们不得不将它们设为 position: absolute 以避免二者同时存在时出现的布局问题。

然而,很多情况下这可能并不符合需求。我们可能想要先执行离开动画,然后在其完成之后再执行元素的进入动画。手动编排这样的动画是非常复杂的,好在我们可以通过向 Transition 组件传入一个 mode prop 定义不同的过渡模式,来实现这个行为:

<Transition mode="out-in">
  ...
</Transition>

mode ,定义过渡模式,用于控制离开/进入过渡的时序。默认情况下是同时的。

mode?: 'in-out' | 'out-in' | 'default'
  • in-out:先执行进入动画,后执行离开动画

  • out-in:先执行离开动画,后执行进入动画

  • default: 同时执行进入动画和离开动画

将之前的例子改为 mode="out-in" ,button 元素便不需要设置 absolute 定位了:

<script src="../../../dist/vue.global.js"></script>

<style>
.btn-container {
  display: inline-block;
  position: relative;
  height: 1em;
}

button {
  /* 
    使用 out-in 过渡模式后,
    不需要将 button 设置为 absolute 定位
  */
  /* position: absolute; */
}

/* 定义进入动画生效状态和离开动画生效状态过渡动效 */
.slide-up-enter-active,
.slide-up-leave-active {
  transition: all 0.25s ease-out;
}

/* 定义进入动画起始状态 */
.slide-up-enter-from {
  opacity: 0;
  transform: translateY(30px);
}

/* 定义离开动画结束状态 */
.slide-up-leave-to {
  opacity: 0;
  transform: translateY(-30px);
}
</style>

<div id="demo">
  <span style="margin-right: 20px">
    Click to cycle through states:
  </span>
  <div class="btn-container">
    <transition
      name="slide-up"
      mode="out-in"
    >
      <button
        v-if="docState === 'saved'"
        @click="docState = 'edited'"
      >Edit</button>
      <button
        v-else-if="docState === 'edited'"
        @click="docState = 'editing'"
      >Save</button>
      <button
        v-else-if="docState === 'editing'"
        @click="docState = 'saved'"
      >Cancel</button>
    </transition>
  </div>
</div>

<script>
const { createApp, ref } = Vue

createApp({
  setup() {
    const docState = ref('saved')    
    return {
      docState,
    }
  }
}).mount('#demo')
</script>

组件间过渡

Transition 组件也可以作用于动态组件之间的切换:

<Transition name="fade" mode="out-in">
  <component :is="activeComponent"></component>
</Transition>

完整的例子如下:

<script src="../../../dist/vue.global.js"></script>

<script type="text/x-template" id="comp-a">
  <div>
    Component A
  </div>
</script>

<script>
// 定义 CompA 组件
const CompA = {
  template: '#comp-a',
}
</script>


<script type="text/x-template" id="comp-b">
  <div>
    Component B
  </div>
</script>

<script>
// 定义 CompB 组件
const CompB = {
  template: '#comp-b',
}
</script>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>
<div id="demo">
	<label>
    <input type="radio" v-model="activeComponent" :value="CompA"> A
  </label>
  <label>
    <input type="radio" v-model="activeComponent" :value="CompB"> B
  </label>
  <transition name="fade" mode="out-in">
    <component :is="activeComponent"></component>
  </transition>
</div>

<script>
const { createApp, shallowRef } = Vue

Vue.createApp({
  setup() {
    const activeComponent = shallowRef(CompA)

    return {
      activeComponent,
      // 将 CompA、CompB 作为 setup 函数的返回值,
      // CompA、CompB 才可以在模板中被访问
      CompA,
      CompB
    }
  }
}).mount('#demo')
</script>

动态过渡

Transition 组件的 props (比如 name)也可以是动态的。这让我们可以根据状态变化动态地应用不同类型的过渡:

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

这个特性的用处是可以提前定义好多组 CSS 过渡或动画的 class,然后在它们之间动态切换。

你也可以根据你的组件的当前状态在 JavaScript 过渡钩子中应用不同的行为。最后,创建动态过渡的终极方式还是创建可复用的过渡组件,并让这些组件根据动态的 props 来改变过渡的效果。可以说,Transition 组件被设计的相当灵活。

使用 Key Attribute 过渡

有时为了触发过渡,你需要强制重新渲染 DOM 元素。

以计数器组件为例:

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

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

<template>
  <Transition>
    <span :key="count">{{ count }}</span>
  </Transition>
</template>

如果不使用 key attribute,则只有文本节点会被更新,因此不会发生过渡。但是,有了 key 属性,Vue 就知道在 count 改变时创建一个新的 span 元素,因此 Transition 组件有两个不同的元素在它们之间进行过渡。

完整的例子如下:

<script src="../../../dist/vue.global.js"></script>

<style>
span{
  font-size: 4rem;
}
.wrapper{
  position:relative;
}

.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
  position: absolute;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}
</style>
<div id="demo">
  <div class="wrapper">
    <transition>
      <span :key="count">{{ count }}</span>
    </transition>
  </div>
</div>

<script>
const { createApp, ref } = Vue

Vue.createApp({
  setup() {
    const count = ref(0)
    setInterval(() => {
      count.value++
    }, 1000)
    return {
      count
    }
  }
}).mount('#demo')
</script>

总结

Transition 组件设计得非常灵活,不仅可以通过 css 实现过渡效果,也可通过 JavaScript 实现过渡效果,还支持出现时过渡、元素间过渡、组件间过渡以及设置过渡模式等。因此 Transition 组件非常值得深入掌握。他会成为你在今后开发过渡动画效果的利器。

pic23.png

Transition 组件支持的 Props:

interface TransitionProps {
  /**
   * 用于自动生成过渡 CSS class 名。
   * 例如 `name: 'fade'` 将自动扩展为 `.fade-enter`、
   * `.fade-enter-active` 等。
   */
  name?: string
  /**
   * 是否应用 CSS 过渡 class。
   * 默认:true
   */
  css?: boolean
  /**
   * 指定要等待的过渡事件类型
   * 来确定过渡结束的时间。
   * 默认情况下会自动检测
   * 持续时间较长的类型。
   */
  type?: 'transition' | 'animation'
  /**
   * 显式指定过渡的持续时间。
   * 默认情况下是等待过渡效果的根元素的第一个 `transitionend`
   * 或`animationend`事件。
   */
  duration?: number | { enter: number; leave: number }
  /**
   * 控制离开/进入过渡的时序。
   * 默认情况下是同时的。
   */
  mode?: 'in-out' | 'out-in' | 'default'
  /**
   * 是否对初始渲染使用过渡。
   * 默认:false
   */
  appear?: boolean

  /**
   * 用于自定义过渡 class 的 prop。
   * 在模板中使用短横线命名,例如:enter-from-class="xxx"
   */
  enterFromClass?: string
  enterActiveClass?: string
  enterToClass?: string
  appearFromClass?: string
  appearActiveClass?: string
  appearToClass?: string
  leaveFromClass?: string
  leaveActiveClass?: string
  leaveToClass?: string
}

Transition 组件支持的事件:

  • @before-enter
  • @before-leave
  • @enter
  • @leave
  • @appear
  • @after-enter
  • @after-leave
  • @after-appear
  • @enter-cancelled
  • @leave-cancelled (v-show only)
  • @appear-cancelled

参考

  1. Transition

  2. Transition API