Vue 3 动效
Vue 内置支持的过渡动效主要是针对元素素进入(显示)/离开(隐藏)和列表(重排)的情况,基于 CSS3 的 transition
和 animation
属性实现的。
提示
对于其他过渡动效,以及在 web 上创建流畅的动画所需考虑的性能因素,可以参考官方文章这一章。
Vue 中常见的触发动画的场景
- 条件渲染
v-if
或v-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
作为选择器,更精细地设置元素的在不同过渡阶段的样式:
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-
<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"
<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>
const Demo = {
data() {
return {
show: true
}
}
}
Vue.createApp(Demo).mount('#demo')
提示
如果使用 animation
属性实现动效(而不是 transition
控制过渡),则一般不需要指定 v-enter-from
/v-leave-from
或 v-enter-to
/v-leave-to
的状态,只需要在 v-enter-active/v-leave-active
过渡中设定 CSS 属性 animation
即可,其中在关键帧中指定了 0%
和 100%
状态时的样式就是开始和结束的状态
<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 的属性 transition
和 animation
设置动画(但不推荐同时使用),就可能因为两种过渡时间不同,比如 animation
很快的被触发并完成了,而 transition
效果还没结束,而出现一些意想不到的动画 Bug
这时候可以为容器添加选项 type
,并设置其值为 animation
或 transition
来显式地声明你需要 Vue 监听的过渡动画类型
<template>
<!-- 可以指定监听哪一种动效类型 -->
<Transition type="animation">
<!-- ... -->
</Transition>
</template>
或者在容器元素上通过 duration
prop 显式地指定持续时间(以毫秒计),也可以用对象的形式来指定进入和离开的过渡动效所持续的时间
<template>
<div>
<!-- 显式指定动效的持续时间 -->
<Transition :duration="1000">
<!-- ... -->
</Transition>
<!-- 分别指定进入和离开的过渡动效所需的时间 -->
<Transition :duration="{ enter: 500, leave: 800 }">
<!-- ... -->
</Transition>
</div>
</template>
后代元素的过渡
虽然过渡 class 仅能应用在 <Transition>
的直接子元素(根元素)上,但可以使用深层级的 CSS 选择器,在深层级的元素上触发过渡效果。
<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:
<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>
// 在元素被插入到 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-if
或 v-show
切换一个元素,也可以通过 v-if
、v-else
、v-else-if
在几个组件间进行切换(只要确保任一时刻只会有一个元素被渲染即可)
在多元素过渡时,默认的过渡模式是两个元素的进入与离开是同时进行的,可能会导致页面布局乱掉
可以通过 mode
prop 采用其他过渡模式
mode="out-in"
旧出新进,即当前元素先进行过渡离开,完成之后新元素过渡进入mode="in-out
" 新进旧出(并不常用)
<transition name="fade" mode="out-in">
<!-- ... the buttons ... -->
</transition>
也可以用于设置动态组件之间的切换过渡模式
<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-if
或 v-show
切换后,都只能渲染一个根节点),要记得其内部的各个节点总是需要提供属性 key
作为唯一标识。
提示
当在 DOM 模板中使用时,组件名需要写为 <transition-group>
<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 手动设置
<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 的动画效果。
<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>
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')
.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>
和组件的插槽功能,创建一个与过渡相关的组件,对过渡效果进行封装和复用
<!-- 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>
一样使用
<template>
<MyTransition>
<div v-if="show">Hello</div>
</MyTransition>
</template>