Skip to content

过渡

Vue 提供了两个内置组件,可以帮助我们处理因状态变化而产生的过渡和动画:

  • <Transition> 用于在元素或组件进入和离开 DOM 时应用动画。本页将介绍它。

  • <TransitionGroup> 用于在 v-for 列表中对元素或组件的插入、移除或移动应用动画。这将在下一章中介绍。

除了这两个组件之外,我们也可以在 Vue 中使用其他技术来应用动画,例如切换 CSS 类,或者通过样式绑定实现由状态驱动的动画。这些额外技术将在动画技巧一章中介绍。

<Transition> 组件

<Transition> 是一个内置组件:这意味着它在任何组件的模板中都可直接使用,无需注册。它可用于对通过默认插槽传入的元素或组件应用进入和离开动画。进入或离开可以通过以下任一方式触发:

  • 通过 v-if 条件渲染
  • 通过 v-show 条件显示
  • 通过 <component> 特殊元素切换动态组件
  • 更改特殊的 key 属性

下面是最基础用法的示例:

template
<button @click="show = !show">切换</button>
<Transition>
  <p v-if="show">你好</p>
</Transition>
css
/* 接下来我们会解释这些类的作用! */
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

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

你好

TIP

<Transition> 只支持单个元素或组件作为其插槽内容。如果内容是组件,那么该组件本身也必须只有一个根元素。

<Transition> 组件中的元素被插入或移除时,会发生以下过程:

  1. Vue 会自动探测目标元素是否应用了 CSS 过渡或动画。如果有,就会在合适的时机添加/移除一些 CSS 过渡类

  2. 如果存在 JavaScript 钩子监听器,那么这些钩子会在合适的时机被调用。

  3. 如果没有检测到 CSS 过渡/动画,并且也没有提供 JavaScript 钩子,那么插入和/或移除的 DOM 操作会在浏览器的下一帧动画中执行。

基于 CSS 的过渡

过渡类

进入/离开过渡会应用六个类。

过渡图示

  1. v-enter-from:进入的起始状态。在元素插入之前添加,在元素插入后一帧移除。

  2. v-enter-active:进入的激活状态。在整个进入阶段都会应用。在元素插入之前添加,在过渡/动画结束时移除。这个类可用于定义进入过渡的持续时间、延迟和缓动曲线。

  3. v-enter-to:进入的结束状态。在元素插入后一帧添加(与移除 v-enter-from 同时),在过渡/动画结束时移除。

  4. v-leave-from:离开的起始状态。在离开过渡触发后立即添加,在一帧后移除。

  5. v-leave-active:离开的激活状态。在整个离开阶段都会应用。在离开过渡触发后立即添加,在过渡/动画结束时移除。这个类可用于定义离开过渡的持续时间、延迟和缓动曲线。

  6. v-leave-to:离开的结束状态。在离开过渡触发后一帧添加(与移除 v-leave-from 同时),在过渡/动画结束时移除。

v-enter-activev-leave-active 让我们能够为进入/离开过渡指定不同的缓动曲线,后续章节会看到相关示例。

命名过渡

可以通过 name prop 为过渡命名:

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

对于命名过渡,其过渡类名前缀会使用其名称而不是 v。例如,上面这个过渡应用的类将是 fade-enter-active 而不是 v-enter-active。fade 过渡对应的 CSS 应该如下所示:

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

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

CSS 过渡

<Transition> 最常与 原生 CSS 过渡配合使用,就像上面的基础示例那样。transition CSS 属性是一个简写属性,允许我们指定过渡的多个方面,包括应该被动画化的属性、过渡持续时间,以及 缓动曲线

下面是一个更高级的示例,它对多个属性进行过渡,并为进入和离开设置不同的持续时间和缓动曲线:

template
<Transition name="slide-fade">
  <p v-if="show">你好</p>
</Transition>
css
/*
  进入和离开动画可以使用不同的
  持续时间和时间函数。
*/
.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 动画的应用方式与 CSS 过渡相同,区别在于 *-enter-from 不会在元素插入后立即移除,而是在 animationend 事件触发时移除。

对于大多数 CSS 动画,我们可以直接将其声明在 *-enter-active*-leave-active 类下。下面是一个示例:

template
<Transition name="bounce">
  <p v-if="show" style="text-align: center;">
    这里有一些弹跳文本!
  </p>
</Transition>
css
.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);
  }
}

你好,这里有一段会弹跳的文字!

自定义过渡类

你也可以通过向 <Transition> 传入以下 props 来指定自定义过渡类:

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

这些会覆盖常规的类名。当你想要将 Vue 的过渡系统与现有的 CSS 动画库结合使用时,这尤其有用,例如 Animate.css

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

同时使用过渡和动画

Vue 需要挂载事件监听器来判断过渡何时结束。根据应用的 CSS 规则类型,这个事件可以是 transitionendanimationend。如果你只使用其中一种,Vue 可以自动检测正确的类型。

不过,在某些情况下,你可能希望在同一个元素上同时使用两者,例如由 Vue 触发的 CSS 动画,以及 hover 时的 CSS 过渡效果。在这种情况下,你需要通过传入 type prop 显式声明你希望 Vue 关注的类型,其值可以是 animationtransition

template
<Transition type="animation">...</Transition>

嵌套过渡与显式过渡时长

虽然过渡类只会应用到 <Transition> 中的直接子元素,但我们仍然可以通过嵌套的 CSS 选择器来对嵌套元素应用过渡:

template
<Transition name="nested">
  <div v-if="show" class="outer">
    <div class="inner">
      你好
    </div>
  </div>
</Transition>
css
/* 作用于嵌套元素的规则 */
.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 已省略 */

我们甚至可以在进入时给嵌套元素添加过渡延迟,从而创建交错的进入动画序列:

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

不过,这会带来一个小问题。默认情况下,<Transition> 组件会通过监听根过渡元素上的第一个 transitionendanimationend 事件,来自动判断过渡何时结束。对于嵌套过渡,期望的行为应该是等待所有内部元素的过渡都结束。

在这种情况下,你可以在 <Transition> 组件上使用 duration prop 显式指定过渡时长(以毫秒为单位)。总时长应当与内部元素的延迟加上过渡时长相匹配:

template
<Transition :duration="550">...</Transition>
你好

在 Playground 中试一试

如果需要,你也可以使用对象形式分别指定进入和离开的时长:

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

性能注意事项

你可能会注意到,上面展示的动画大多使用了 transformopacity 之类的属性。这些属性之所以高效,是因为:

  1. 它们不会在动画过程中影响文档布局,因此不会在每一帧动画中触发昂贵的 CSS 布局计算。

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

相比之下,像 heightmargin 这样的属性会触发 CSS 布局,因此动画成本要高得多,使用时应当谨慎。

JavaScript 钩子

你可以通过监听 <Transition> 组件上的事件,使用 JavaScript 介入过渡过程:

template
<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) {}

// 在元素插入后的一帧调用。
// 用于开始进入动画。
function onEnter(el, done) {
  // 调用 done 回调以表示过渡结束
  // 与 CSS 结合使用时可选
  done()
}

// 在进入过渡完成时调用。
function onAfterEnter(el) {}

// 在进入过渡于完成前被取消时调用。
function onEnterCancelled(el) {}

// 在离开钩子之前调用。
// 大多数情况下,你只需要使用 leave 钩子
function onBeforeLeave(el) {}

// 在离开过渡开始时调用。
// 用于开始离开动画。
function onLeave(el, done) {
  // 调用 done 回调以表示过渡结束
  // 与 CSS 结合使用时可选
  done()
}

// 在离开过渡完成且
// 元素已从 DOM 中移除时调用。
function onAfterLeave(el) {}

// 仅在 v-show 过渡中可用
function onLeaveCancelled(el) {}
js
export default {
  // ...
  methods: {
    // 在元素插入 DOM 之前调用。
    // 用于设置元素的 "enter-from" 状态
    onBeforeEnter(el) {},

    // 在元素插入后的一帧调用。
    // 用于开始动画。
    onEnter(el, done) {
      // 调用 done 回调以表示过渡结束
      // 与 CSS 结合使用时可选
      done()
    },

    // 在进入过渡完成时调用。
    onAfterEnter(el) {},

    // 在进入过渡于完成前被取消时调用。
    onEnterCancelled(el) {},

    // 在离开钩子之前调用。
    // 大多数情况下,你只需要使用 leave 钩子。
    onBeforeLeave(el) {},

    // 在离开过渡开始时调用。
    // 用于开始离开动画。
    onLeave(el, done) {
      // 调用 done 回调以表示过渡结束
      // 与 CSS 结合使用时可选
      done()
    },

    // 在离开过渡完成且
    // 元素已从 DOM 中移除时调用。
    onAfterLeave(el) {},

    // 仅在 v-show 过渡中可用
    onLeaveCancelled(el) {}
  }
}

这些钩子可以与 CSS 过渡 / 动画结合使用,也可以单独使用。

当使用仅 JavaScript 的过渡时,通常最好添加 :css="false" 属性。这会明确告诉 Vue 跳过自动 CSS 过渡检测。除了性能会稍微更好一些之外,这也可以防止 CSS 规则意外干扰过渡:

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

使用 :css="false" 时,我们还需要完全负责控制过渡何时结束。在这种情况下,@enter@leave 钩子都需要 done 回调。否则,这些钩子会同步调用,过渡会立即完成。

这里有一个使用 GSAP 库 执行动画的演示。当然,你也可以使用任何你想要的其他动画库,例如 Anime.jsMotion One

可复用过渡

过渡可以通过 Vue 的组件系统进行复用。要创建一个可复用过渡,我们可以创建一个组件来包裹 <Transition> 组件,并向下传递插槽内容:

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

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

<style>
/*
  必要的 CSS...
  注意:这里避免使用 <style scoped>,因为它
  不适用于插槽内容。
*/
</style>

现在可以像使用内置版本一样导入并使用 MyTransition

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

出现时过渡

如果你还想对节点的初始渲染应用过渡,可以添加 appear 属性:

template
<Transition appear>
  ...
</Transition>

元素间过渡

除了使用 v-if / v-show 切换元素之外,我们还可以使用 v-if / v-else / v-else-if 在两个元素之间过渡,只要我们确保在任何时刻只显示一个元素即可:

template
<Transition>
  <button v-if="docState === 'saved'">编辑</button>
  <button v-else-if="docState === 'edited'">保存</button>
  <button v-else-if="docState === 'editing'">取消</button>
</Transition>
点击循环切换状态:

在 Playground 中试试

过渡模式

在前面的例子中,进入和离开的元素是同时播放动画的,因此我们不得不将它们设为 position: absolute,以避免当两个元素同时存在于 DOM 中时产生布局问题。

不过,在某些情况下这并不可行,或者说并不是我们想要的行为。我们可能希望先让离开的元素完成动画,再让进入的元素在离开动画完成之后才插入。如果手动协调这类动画会非常复杂——幸运的是,我们可以通过给 <Transition> 传入 mode 属性来启用这种行为:

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

下面是使用 mode="out-in" 的前一个演示:

点击循环切换状态:

<Transition> 也支持 mode="in-out",尽管它的使用频率要低得多。

组件间过渡

<Transition> 也可以包裹 动态组件 使用:

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

动态过渡

name 这样的 <Transition> 属性也可以是动态的!它允许我们根据状态变化动态应用不同的过渡效果:

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

当你使用 Vue 的过渡类约定定义了 CSS 过渡 / 动画,并且想在它们之间切换时,这会很有用。

你也可以根据组件的当前状态,在 JavaScript 过渡钩子中应用不同的行为。最后,创建动态过渡的终极方式是通过可复用过渡组件,它们接受 props 来改变所使用过渡的性质。听起来可能有点老套,但真正的限制其实只有你的想象力。

带有 Key 属性的过渡

有时你需要强制某个 DOM 元素重新渲染,才能触发过渡效果。

以这个计数器组件为例:

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

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

<template>
  <Transition>
    <span :key="count">{{ count }}</span>
  </Transition>
</template>
vue
<script>
export default {
  data() {
    return {
      count: 1,
      interval: null 
    }
  },
  mounted() {
    this.interval = setInterval(() => {
      this.count++;
    }, 1000)
  },
  beforeDestroy() {
    clearInterval(this.interval)
  }
}
</script>

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

如果我们省略了 key 属性,只有文本节点会被更新,因此不会发生过渡。然而,有了 key 属性后,Vue 就知道每当 count 改变时都创建一个新的 span 元素,因此 Transition 组件就有了两个不同的元素之间可以进行过渡。


相关内容

过渡 has loaded