Vue 3 动效

Vue 内置支持的过渡动效主要是针对元素素进入(显示)/离开(隐藏)和列表(重排)的情况,基于 CSS3 的 transitionanimation 属性实现的。

提示

对于其他过渡动效,以及在 web 上创建流畅的动画所需考虑的性能因素,可以参考官方文章这一章

Vue 中常见的触发动画的场景

  • 条件渲染 v-ifv-show 切换,动态组件属性 is 切换
  • 列表更新
  • 状态更新

元素进入/离开过渡

Vue 提供内置组件 <Transition name="animationType"></Transition> 作为容器,然后就可以有多种方法为包裹在其中的(直接子元素)元素设置进入/离开时的动画

  • 一种是基于 CSS:Vue 会自动为发生过渡的元素添加特定的 class(并在结束后移除相应的 class 属性),可以基于这些 class 属性使用 CSS 实现动画。而且可以定制 class 的属性值,便于集成第三方 CSS 动画库,如 animate.css
  • 另一种是基于 JS:Vue 提供了一些与过渡相关的钩子函数,它们会在发生过渡的同时触发,便于通过 JS 实现动画,也可以集成第三方 JavaScript 动画库

CSS 过渡

Vue 会在过渡的不同阶段,为元素插入 6 种不同后缀的 class 属性,可以使用这些 class 作为选择器,更精细地设置元素的在不同过渡阶段的样式:

transition
transition

  • v-enter-from 定义进入过渡的开始状态,在元素被插入之前生效,在元素被插入之后的下一帧移除。
  • v-enter-active 在整个进入过渡的阶段中应用,在过渡/动画完成之后移除。可以使用这个类名作为 CSS 选择器设置过渡效果,例如通过设置 CSS 属性 transition 定义进入过渡的过程时间,延迟或曲线函数。
  • v-enter-to 定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。
  • v-leave-from 定义离开过渡的开始状态,在离开过渡被触发时立刻生效,下一帧被移除。
  • v-leave-active 在整个离开过渡的阶段中应用,在过渡/动画完成之后移除。可以使用这个类名作为 CSS 选择器设置过渡效果,例如通过设置 CSS 属性 transition 定义离开过渡的过程时间,延迟和曲线函数。
  • v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被删除),在过渡/动画完成之后移除。
提示

动效如果在初次加载页面时不生效,可以为元素 <transition> 添加属性 appear 以设置节点初始渲染的过渡

提示

以上 6 种不同后缀的 class 属性都使用了 v- 作为默认前缀,如果容器标签设置了属性 name<transition name="animationType">,则动态插入的 class 属性名称就会以 animationType- 为前缀

v-enter 会替换为 animationType-enter,记得使用 CSS class 设置样式时使用相应的前缀,而不是默认前缀 v-

vue
<template>
  <div id="demo">
    <button @click="show = !show">
      Toggle show
    </button>
    <transition name="slide-fade">
      <p v-if="show">hello</p>
    </transition>
  </div>
</template>
<script>
const Demo = {
  data() {
    return {
      show: true
    }
  }
}
Vue.createApp(Demo).mount('#demo')
</script>
<style>
/* 可以设置不同的进入和离开动画 */
/* 设置持续时间和动画函数 */
.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;
}
</style>

自定义过渡 class 类名的前缀

如果要用第三方的 CSS 动画库,如 Animate.css,一般需要在 DOM 元素上插入特定的类名来应用相应的 CSS 样式,可以在标签 <transition></transition> 上使用如下列出的 attribute,这样 Vue 会在过渡的相应阶段为容器内的变更的(直接子元素)元素添加相应的自定义过渡类名

  • enter-from-class="custom-class-name" 在元素进入页面前添加的类名
  • enter-active-class="custom-class-name"
  • enter-to-class="custom-class-name"
  • leave-from-class="custom-class-name"
  • leave-active-class="custom-class-name"
  • leave-to-class="custom-class-name"
html
<link
  href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.0/animate.min.css"
  rel="stylesheet"
  type="text/css"
/>
<div id="demo">
  <button @click="show = !show">
    Toggle render
  </button>
  <transition
    name="custom-classes-transition"
    enter-active-class="animate__animated animate__tada"
    leave-active-class="animate__animated animate__bounceOutRight"
  >
    <p v-if="show">hello</p>
  </transition>
</div>
js
const Demo = {
  data() {
    return {
      show: true
    }
  }
}
Vue.createApp(Demo).mount('#demo')
提示

如果使用 animation 属性实现动效(而不是 transition 控制过渡),则一般不需要指定 v-enter-from/v-leave-fromv-enter-to/v-leave-to 的状态,只需要在 v-enter-active/v-leave-active 过渡中设定 CSS 属性 animation 即可,其中在关键帧中指定了 0%100% 状态时的样式就是开始和结束的状态

vue
<template>
  <div id="demo">
    <button @click="show = !show">
      Toggle show
    </button>
    <transition name="bounce">
      <p v-if="show">hello</p>
    </transition>
  </div>
</template>
<script>
const Demo = {
  data() {
    return {
      show: true
    }
  }
}
Vue.createApp(Demo).mount('#demo')
</script>
<style>
.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);
  }
}
</style>

同时使用 transition 和 animation

Vue 可以自动得出过渡效果的完成时机,以「拔除」添加到元素上的相关的 class 类名

但是如果同时使用了 CSS 的属性 transitionanimation 设置动画(但不推荐同时使用),就可能因为两种过渡时间不同,比如 animation 很快的被触发并完成了,而 transition 效果还没结束,而出现一些意想不到的动画 Bug

这时候可以为容器添加选项 type,并设置其值为 animationtransition显式地声明你需要 Vue 监听的过渡动画类型

vue
<template>
  <!-- 可以指定监听哪一种动效类型 -->
  <Transition type="animation">
    <!-- ... -->
  </Transition>
</template>

或者在容器元素上通过 duration prop 显式地指定持续时间(以毫秒计),也可以用对象的形式来指定进入和离开的过渡动效所持续的时间

vue
<template>
  <div>
    <!-- 显式指定动效的持续时间 -->
    <Transition :duration="1000">
      <!-- ... -->
    </Transition>
    <!-- 分别指定进入和离开的过渡动效所需的时间 -->
    <Transition :duration="{ enter: 500, leave: 800 }">
      <!-- ... -->
    </Transition>
  </div>
</template>

后代元素的过渡

虽然过渡 class 仅能应用在 <Transition> 的直接子元素(根元素)上,但可以使用深层级的 CSS 选择器,在深层级的元素上触发过渡效果

vue
<template>
  <Transition name="nested">
    <div v-if="show" class="outer">
      <div class="inner">
        Hello
      </div>
    </div>
  </Transition>
</template>
<style>
/* 应用于嵌套元素的规则 */
.nested-enter-active .inner,
.nested-leave-active .inner {
  /* 可以为嵌套的后代元素设置延迟以获得交错效果 */
  transition-delay: 0.25s;
  transition: all 0.3s ease-in-out;
}
.nested-enter-from .inner,
.nested-leave-to .inner {
  transform: translateX(30px);
  opacity: 0;
}
</style>

但是默认情况下 <Transition> 组件会通过监听过渡根元素上的第一个 transitionend 或者 animationend 事件来尝试自动判断过渡何时结束,而在嵌套的过渡中,期望的行为应该是等待所有内部元素的过渡完成(而不是根元素动效结束就「拔除」相应的 class),这种默认行为会带来一些动画 bug

可以通过向 <Transition> 组件传入 duration prop 来显式指定过渡的持续时间来修正 bug,总持续时间应该是动效的延迟时间和过渡持续,例如 <Transition :duration="550">...</Transition>

钩子函数

Vue 会在过渡的不同阶段同时触发相应事件,可以在内置组件 <transition> 监听这些事件,然后通过 JS 在事件处理函数中「手动」控制动画,一般是设置元素的 attribute 样式,或「对接」其他动画框架,例如 gsap

html
<transition
  @before-enter="onBeforeEnter"
  @enter="onEnter"
  @after-enter="onAfterEnter"
  @enter-cancelled="onEnterCancelled"
  @before-leave="onBeforeLeave"
  @leave="onLeave"
  @after-leave="onAfterLeave"
  @leave-cancelled="onLeaveCancelled"
  :css="false"
>
  <!-- ... -->
</transition>
js
// 在元素被插入到 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) {}
  • @before-enter="onBeforeEnter(el)" 可以传递执行动画的元素
  • @enter="onEnter(el, done)" 需要在回调函数中,在动画执行执行完成时调用 done() 告知 Vue 动画结束,必须执行一次 done(),否则它们将被同步调用,过渡会立即完成
  • @after-enter="onAfterEnter"
  • @enter-cancelled="onEnterCancelled" 元素离开页面触发的事件
  • @before-leave="onBeforeLeave"
  • @leave="onLeave" 需要在回调函数中,在动画执行执行完成时调用 done() 告知 Vue 动画结束,必须执行一次 done(),否则它们将被同步调用,过渡会立即完成
  • @after-leave="onAfterLeave"
  • @leave-cancelled="onLeaveCancelled"
提示

虽然这些钩子函数可以结合 CSS transitions/animations 使用,也可以单独使用。

在使用仅由 JavaScript 执行的动画时,推荐为 <transition> 添加 v-bind:css="false" prop 显示地向 Vue 表明可以会跳过 CSS 过渡的探测,这也可以避免过渡过程中受 CSS 的干扰,这种情况下对于 @enter@leave 钩子的回调函数种 done() 就是必须的(否则钩子将被同步调用,过渡将立即完成)

过渡模式

除了通过 v-ifv-show 切换一个元素,也可以通过 v-ifv-elsev-else-if 在几个组件间进行切换(只要确保任一时刻只会有一个元素被渲染即可)

在多元素过渡时,默认的过渡模式是两个元素的进入与离开是同时进行的,可能会导致页面布局乱掉

可以通过 mode prop 采用其他过渡模式

  • mode="out-in" 旧出新进,即当前元素先进行过渡离开,完成之后新元素过渡进入
  • mode="in-out" 新进旧出(并不常用)
html
<transition name="fade" mode="out-in">
  <!-- ... the buttons ... -->
</transition>

也可以用于设置动态组件之间的切换过渡模式

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

对于使用 v-if/v-else 实现的多元素之间进行切换时,Vue 3 已经默认为 v-if/v-else/v-else-if 的各分支项自动生成唯一的 key,因此这些元素的 key 属性不再必要了,如果自己添加 key 属性时需要设置唯一可区分的值,否则相同标签名的元素之间切换时只会替换内部的内容,而不会进行触发整个元素切换过渡。

列表重排过渡

使用内置组件 <TransitionGroup></TransitionGroup> 作为容器,其内可以有多个节点**(而 <transition> 只允许一个根节点,虽然可以包裹多个元素,但是每次 v-ifv-show 切换后,都只能渲染一个根节点),要记得其内部的各个节点总是需要提供属性 key 作为唯一标识。

提示

当在 DOM 模板中使用时,组件名需要写为 <transition-group>

html
<transition-group name="list" tag="p">
  <span v-for="item in items" :key="item" class="list-item">
    {{ item }}
  </span>
</transition-group>

Vue 也是在过渡的不同阶段,为元素(是指列表对应的元素,而不是容器元素)插入以上所述的 6 个不同后缀 class,可以使用这些 class 作为选择器设置 CSS 动画

  • v-enter-from
  • v-enter-active
  • v-enter-to
  • v-leave-from
  • v-leave-active
  • v-leave-to
说明

过渡模式在 <TransitionGroup> 里并不可用,因为不再是在互斥的元素之间进行切换。

移动动画

当列表中某一项被插入或移除时,以上的 class 类名只会插入到发生变化的列表项,而它周围的元素则会立即移动,从视觉上来看就是发生「跳跃」而不是平稳地移动

Vue 3 新增一个额外CSS 规则,为发生移动的列表项插入后缀为 v-move 的 class 类属性,以此来解决上述问题

说明

如果为 <TransitionGroup> 设置了 name 属性 <transition-group name="animationType"></transition-group>,则列表重排时插入到列表项的 class 类属性的前缀就会改变,即为 animationType-move

还可以像之前的类名一样,即通过 move-class attribute 手动设置

vue
<template>
  <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item">
      {{ item }}
    </li>
  </TransitionGroup>
</template>
<style>
.list-move, /* 对移动中的元素应用的过渡 */
.list-enter-active,
.list-leave-active {
  transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}
/* 确保将离开的元素从布局流中删除
  以便能够正确地计算移动的动画。 */
.list-leave-active {
  position: absolute;
}
</style>

一般会通过 v-move 这个选择器为移动的列表元素设置 CSS 属性 transform,指定过渡 timing 和 easing 曲线,然后 Vue 会将元素从之前的位置平滑过渡新的位置,实现一个称为 FLIP 的动画效果。

html
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<div id="flip-list-demo">
  <button @click="shuffle">Shuffle</button>
  <transition-group name="flip-list" tag="ul">
    <li v-for="item in items" :key="item">
      {{ item }}
    </li>
  </transition-group>
</div>
js
const Demo = {
  data() {
    return {
      items: [1, 2, 3, 4, 5, 6, 7, 8, 9]
    }
  },
  methods: {
    shuffle() {
      // 使用 lodash 的 shuffle 对列表元素进行重排
      this.items = _.shuffle(this.items)
    }
  }
}
Vue.createApp(Demo).mount('#flip-list-demo')
css
.flip-list-move {
  transition: transform 0.8s ease;
}
提示

FLIP 动效对于 display: inline 元素无效,对于行内元素,如 <span>,可以设置为 display: inline-block

说明

在 Vue 3 中,<Transition-group> 默认不再渲染根节点,但可以通过属性 tag 来设置,例如 tag="div" 将使用 <div> 元素作为容器包装列表。

动态过渡

Vue 是数据驱动画面的,因此过渡动效的类型也是可以基于数据进行变化的

可以通过 <Transition :name="transitionName"> 将属性 name 绑定到变量,就可以实时动态操作过渡的类型,然后在不同的 JavaScript 过渡钩子设置变量 transitionName 的值

因为事件钩子是方法,它们可以访问任何上下文中的数据,这意味着根据组件的状态不同,过渡可以有不同的表现,例如实现轮播图点击左右箭头时可以实现切换方向和过渡的不同。

可复用过渡效果

可以基于 Vue 内置组件 <Transition> 和组件的插槽功能,创建一个与过渡相关的组件,对过渡效果进行封装和复用

vue
<!-- 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> 可以在导入后,像内置组件 <Transition> 一样使用

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

状态过渡

对于元素的内容/数据本身的改变也可以设置过渡动效,包括数字和运算、颜色的显示、SVG 节点的位置、元素的大小和其他的 property。

一般以数值表示的样式属性都可以设置动效,可以结合 Vue 的响应式和组件系统,使用第三方库来实现切换元素的过渡状态,如 tween.jsgsap


Copyright © 2024 Ben

Theme BlogiNote

Icons from Icônes