内置组件
Transition
Vue提供了两个内置组件,可以帮助你制作基于状态变化的过渡和动画:
<Transition> 会在一个元素或组件进入和离开DOM时应用动画。
<TransitionGroup>会在一个v-for列表中的元素或组件被插入,移动,或移除时应用动画。
除了这两个组件,也可以通过其他技术手段来应用动画,比如切换CSS class或用状态绑定样式来驱动动画。
<Transition>组件
<Transition>是一个内置组件,这意味着它在任意别的组件中都可以被使用,无需注册。它可以将进入和离开动画应用到通过默认插槽传递给它的元素或组件上。进入或离开可以由以下的条件之一触发:
- 由 v-if 所触发的切换
- 由 v-show 所触发的切换
- 由特殊元素 <component> 切换的动态组件
- 改变特殊的 key 属性
以下展示了最基本用法的示例:
<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;
}
提示
<Transition> 仅支持单个元素或组件作为其插槽内容,如果内容是一个组件,这个组件必须仅有一个根元素。
当一个 <Transition> 组件中的元素被插入或移除时,会发生下面这些事情:
- Vue会自动检测目标元素是否应用了CSS 过渡或动画。如果是,则一些CSS过渡 class 会在适当的时机被添加和移除。
- 如果有作为监听器的JavaScript钩子,这个钩子函数会在适当时机被调用。
- 如果没有探测到CSS过渡或动画、也没有提供JavaScript钩子,那么DOM的插入、删除操作将作为浏览器的下一个动画帧后执行。
基于CSS的过渡效果
CSS 过渡class
一共有6个应用于进入与离开过渡效果的CSS class。
- 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-active 和 v-leave-active 给我们提供了为进入和离开动画指定不同速度曲线的能力。
为过渡效果命名
我们可以给 <Transition> 组件传一个 name prop来声明一个过渡效果名:
<Transition name="fade">
...
</Transition>
对于一个有名字的过渡效果,对它起作用的过渡class会以其名字而不是v作为前缀。例如,上方例子中被应用的 class 将会是 fade-enter-active 而不是 v-enter-active。这个“fade”过渡的class应该是这样:
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
CSS的 transition
<Transition> 一般都会搭配原生CSS过渡一起使用。这个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不是在元素插入后立即移除,而是在一个 animationed 事件触发时被移除。
对于大多数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
你也可以向<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:
<script setup>
import { ref } from 'vue'
const show = ref(true)
</script>
<template>
<button @click="show = !show">Toggle</button>
<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>
</template>
<style>
@import "https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css";
</style>
同时使用transition 和 animation
Vue需要附加事件监听器,以便知道过渡何时结束。可以是transitoned 或 animationed,这取决于你所应用的CSS规则。如果你仅仅使用二者的其中之一,Vue可以自动探测到正确的类型。
然而在某些场景中,你或许想要在同一个元素上同时使用它们两个。举例来说,Vue触发了一个CSS动画,同时鼠标悬停触发另一个CSS过渡。此时你需要显示地传入type prop来声明,告诉Vue需要关心哪种类型,传入的值是 animation 或 transition:
<Transition type="animation">...</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 事件来尝试自动判断过渡何时结束。而在嵌套的过渡中,期望的行为应该是等待所有内部元素的过渡完成。
在这种情况下,可以通过向<Transition>组件传入 duration prop来显式指定过渡的持续事件(以毫秒为单位)。总持续时间应该匹配延迟加上内部元素的过渡持续时间:
<Transition :duration="550">...</Transition>
如果有必要的话,你也可以用对象的形式传入,分开指定进入和离开所需的时间:
<Transition :duration="{ enter: 500, leave: 800 }">...</Transition>
性能考量
你可能注意到上述例子中展示的动画所用到的CSS属性大多是 transform 和 opacity 之类的。用这些属性制作动画非常高效,因为:
- 他们在动画过程中不会影响到DOM结构,因此不会每一帧都触发昂贵的CSS布局重新计算。
- 大多数现代浏览器都可以在执行 transform 动画时利用GPU进行加速。
相比之下,像 height 或者 margin 这样的属性会触发CSS布局变动,因此执行它们的动画效果更昂贵,需要谨慎使用。
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规则意外地干扰过渡效果:
<Transition
...
:css="false"
>
...
</Transition>
在有了 :css = "false" 后,我们就自己全权负责控制什么时候过渡结束了。这种情况下对于@enter 和 @leave 钩子来说,回调函数 done 就是必须的。 否则,钩子将被同步调用,过渡将立即完成。
可复用过渡效果
得益于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>
过渡模式
在之前的例子中,进入和离开的元素都是在同时开始时动画的,因此我们不得不将他们设置为 position:absolute 以避免二者同时出现的布局问题。
然而,很多情况下这可能并不符合要求。我们可能想要先执行离开动画,然后在其完成之后再执行元素的进入动画。手动编排这样的动画是非常复杂的,好在我们可以通过向 <Transition> 传入一个 mode prop 来实现这个行为:
<Transition mode="out-in">
...
</Transition>
组件间过渡
<Transition> 也可以作用于动态组件之间的切换:
<Transition name="fade" mode="out-in">
<component :is="activeComponent"></component>
</Transition>
动态过渡
<Transition> 的props(比如name)也可以是动态的!这让我们可以根据状态变化动态地应用不同类型的过渡:
<Transition :name="transitionName">
<!-- ... -->
</Transition>
这个特性的用处是可以提前定义好多组 CSS 过渡或动画的class,然后在它们之间动态切换。
你也可以根据你的组件的当前状态在JavaScript 过渡钩子中应用不同的行为。最后,创建动态过渡的终极方式还是创建可复用的过渡组件,并让这些组件根据动态的props来过渡改变的效果。
使用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 组件有两个不同的元素在它们之间进行过渡。