重学Vue3-对动画的支持

481 阅读8分钟

Vue 提供了两个内置组件,可以帮助你制作基于状态变化的过渡和动画:

  • <Transition> 会在一个普通html元素或组件进入和离开 DOM 时应用动画。
  • <TransitionGroup> 会在一个 v-for 列表中的元素或组件被插入,移动,或移除时应用动画

<Transition> 组件

触发条件

  • 由 v-if 所触发的切换
  • 由 v-show 所触发的切换
  • 由特殊元素 <component> 切换的动态组件
  • 改变特殊的 key 属性

<Transition> 仅支持单个元素或组件作为其插槽内容。如果内容是一个组件,这个组件必须仅有一个根元素。

<button @click="show = !show">Toggle</button>
<Transition>
  <p v-if="show">hello</p>
</Transition>

/* 下面我们会解释这些 class 是做什么的 */
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

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

一共有 6 个应用于进入与离开过渡效果的 CSS class。最中间两个class一般不用,因为元素的样式已经固定了

注意区分进入动画 退出动画,分开来看

image.png

  • v-enter-from

    • 进入 动画的 起始状态
    • 在元素插入之前添加,在元素插入完成后的 下一帧移除
  • v-enter-active

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

    • 进入 动画的 结束状态
    • 在元素插入完成后的下一帧被添加 (也就是 v-enter-from 被移除的同时),在过渡或动画完成之后移除
  • v-leave-from

    • 离开 动画的 起始状态
    • 在离开过渡效果被触发时立即添加,在一帧后被移除
  • v-leave-active

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

    • 离开 动画的 结束状态
    • 在一个离开动画被触发后的 下一帧 被添加 (即 v-leave-from 被移除的同时),在 过渡或动画完成之后移除

如果是一个元素的显示隐藏,v-enter-from v-leave-to的内容一般是相同的,多个则是不同的

如果传入一个name,class的类名就不需要以v-开头

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

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

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

核心原理

  • 当 组件(DOM) 被 挂载 时,将过渡动效添加到该 DOM 元素上
  • 当 组件(DOM) 被 卸载 时,不是直接卸载,而是等待附加到 DOM 元素上的 动效执行完成,然后在真正执行卸载操作,即 延迟卸载时机

CSS 的 transition

<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 的 animation

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

对于大多数的 CSS 动画,我们可以简单地在 *-enter-active 和 *-leave-active class 下声明它们。下面是一个示例:

<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

在集成其他的第三方 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>

JavaScript 钩子

你可以通过监听 <Transition> 组件事件的方式在过渡过程中挂上钩子函数:

<Transition
  @before-enter="onBeforeEnter"
  @enter="onEnter"
  @after-enter="onAfterEnter"
  @enter-cancelled="onEnterCancelled"
  @before-leave="onBeforeLeave"
  @leave="onLeave"
  @after-leave="onAfterLeave"
  @leave-cancelled="onLeaveCancelled"
>
  <!-- ... -->
</Transition>

// 在元素被插入到 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) {}

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

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

出现时过渡

如果你想在某个节点初次渲染时应用一个过渡效果,你可以添加 appear prop,这样页面第一次渲染就会有动画效果:

<Transition appear>
  ...
</Transition>

元素间过渡

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

 <div class="btn-container">//relative
		<Transition name="slide-up">
      <button v-if="docState === 'saved'"    //absolute
              @click="docState = 'edited'">Edit</button>
      <button v-else-if="docState === 'edited'"     //absolute
              @click="docState = 'editing'">Save</button>
      <button v-else-if="docState === 'editing'"    //absolute
              @click="docState = 'saved'">Cancel</button>
    </Transition>
  </div>

因此我们一般都想先执行离开动画,然后在其完成之后再执行元素的进入动画.我们可以通过向 <Transition> 传入一个 mode prop 来实现这个行为

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

反过来就是mode="in-out"但是不常用

所以当有多个元素的动画的应该使用这个属性,如果只是一个元素的显示隐藏就不需要这么麻烦了.

使用 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 组件有两个不同的元素在它们之间进行过渡。

在没有 key 的情况下,Vue 将使用一种最小化元素移动的算法,并尽可能地就地更新/复用相同类型的元素。如果传了 key,则将根据 key 的变化顺序来重新排列元素,并且将始终移除/销毁 key 已经不存在的元素。

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

尽管过渡 class 仅能应用在 <Transition> 的直接子元素上,我们还是可以使用深层级的 CSS 选择器,在深层级的元素(这里是类名为inner的div)上触发过渡效果:

<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 事件来尝试自动判断过渡何时结束。而在嵌套的过渡中,期望的行为应该是等待所有内部元素的过渡完成。

在这种情况下,你可以通过向 <Transition> 组件传入 duration prop 来显式指定过渡的持续时间 (以毫秒为单位)。总持续时间应该匹配延迟加上内部元素的过渡持续时间:

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

TransitionGroup

<TransitionGroup> 是一个内置组件,用于对 v-for 列表中的元素或组件的插入、移除和顺序改变添加动画效果。

和 <Transition> 的区别

<TransitionGroup> 支持和 <Transition> 基本相同的 props、CSS 过渡 class 和 JavaScript 钩子监听器,但有以下几点区别:

  • 默认情况下,它不会渲染一个容器元素。但你可以通过传入 tag prop 来指定一个元素作为容器元素来渲染。
  • 过渡模式在这里不可用,因为我们不再是在互斥的元素之间进行切换。
  • 列表中的每个元素都必须有一个独一无二的 key attribute。
  • CSS 过渡 class 会被应用在列表内的元素上,而不是容器元素上。

更多内容

对于那些不是正在进入或离开 DOM 的元素,Vue也提供了一些技巧