Vue 的 <Transition>/<TransitionGroup> 的类名和 JS 钩子怎样工作?
<Transition> 在元素进入和离开时依次加上 v-enter-from、v-enter-active、v-enter-to 等六个类名,你只需写 CSS;name 改前缀,mode="out-in" 让旧元素先离开。JS 钩子如 @enter(el, done) 用于 JS 动画,:css="false" 时必须调用 done。<TransitionGroup> 用于 v-for 列表,每项必须有 key,没有 mode,额外提供 v-move 处理位置变化。
简答
<Transition>用来给一个元素的出现和消失加动画。触发条件是v-if、v-show、<component :is>切换,或者key变化。它不写动画本身,只在合适的时机给元素加、删类名,动画由你写的 CSS 完成。进入时有三个类:
v-enter-from是起点,插入前加、一帧后删;v-enter-active贯穿整个进入过程,用来写transition时长和缓动;v-enter-to是终点。离开同理,是v-leave-from、v-leave-active、v-leave-to。写了name="fade",前缀v就换成fade。想用 JS 控制动画时,用
@before-enter、@enter、@leave这些钩子。@enter(el, done)里开始动画,结束时调done()告诉 Vue 完成了。纯 JS 动画加:css="false",这时done必须调。
<TransitionGroup>给v-for列表用,类名加在每一项上,每项必须有唯一key,不支持mode。另外它多一个v-move类,让其他项在列表变化时平滑移动,而不是跳过去。
六个类名分别在什么时候出现
先看最常见的淡入淡出:
<template>
<button @click="show = !show">Toggle</button>
<Transition>
<p v-if="show">hello</p>
</Transition>
</template>
<style>
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
为什么只写了 opacity: 0,没写 opacity: 1?以进入为例。元素插入前,Vue 同时加上 v-enter-from 和 v-enter-active,这时透明度是 0。一帧以后删掉 v-enter-from、加上 v-enter-to,透明度回到默认的 1。因为 v-enter-active 上有 transition,这个变化就被动画化了。等过渡结束,Vue 删掉 -active 和 -to。
离开反过来:触发离开时立即加上 v-leave-from 和 v-leave-active,一帧后换成 v-leave-to,透明度从 1 动画到 0,结束后元素才被真正移除。
Vue 怎么知道“结束了”?默认监听元素上第一个 transitionend 或 animationend 事件。如果嵌套了多层动画,第一个结束事件来得太早,可以用 :duration="550" 或 :duration="{ enter: 500, leave: 800 }" 显式指定毫秒数。
name、mode 和 appear 解决什么问题
一个页面有好几种动画时,用 name 区分。<Transition name="fade"> 对应的类名就是 fade-enter-active 等。和 Animate.css 这类现成库配合时,可以用 enter-active-class 这类 prop 直接指定类名。
两个元素来回切换时,默认是旧的离开、新的进入同时进行,两者会叠在一起。mode="out-in" 让旧元素先离开,离开完再插入新元素,切换组件时最常用:
<Transition name="fade" mode="out-in">
<component :is="activeComponent"></component>
</Transition>
<Transition> 里一次只能有一个元素或组件,组件也必须只有一个根元素。v-if、v-else 切换没问题,因为同一时刻只显示一个。
还有一个容易漏的点:数字变化想要动画,只改文字是不会触发的,因为 Vue 只更新了文本节点,元素没换。给元素加上 :key="count",每次值变了就会生成新元素,才有进入和离开可言。
首次渲染默认没有动画,需要的话加 appear。
用 JS 钩子写动画
用 GSAP 这类 JS 动画库时,用钩子拿到元素自己控制:
<Transition :css="false" @before-enter="onBeforeEnter" @enter="onEnter" @leave="onLeave">
<div v-if="show">...</div>
</Transition>
onBeforeEnter(el) 在插入前调用,用来设置起始状态;onEnter(el, done) 在插入后一帧调用,在这里开始动画,动画结束时调 done();onLeave(el, done) 同理。另外还有 @after-enter、@enter-cancelled、@before-leave、@after-leave 和只在 v-show 下可用的 @leave-cancelled。
为什么 :css="false" 时必须调 done?因为这时 Vue 不再检测 CSS 过渡,也就不知道动画什么时候结束,只能靠你通知。不调的话,钩子会被同步执行,过渡立刻结束,离开的元素马上就没了。和 CSS 过渡一起用时 done 是可选的。:css="false" 还有个好处,能跳过 CSS 检测,也避免 CSS 规则意外干扰 JS 动画。
TransitionGroup 有什么不同
列表的增删要用 <TransitionGroup>,它支持和 <Transition> 相同的 prop、类名和钩子,但有几处区别:
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item">{{ item }}</li>
</TransitionGroup>
.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;
}
tag="ul" 指定外层渲染成什么元素,不写就不渲染外层元素。类名加在每个 <li> 上,不加在容器上。每项必须有唯一 key,Vue 靠它判断哪一项是新增、哪一项被删,key 的作用见《列表中的 key 为什么不能随意使用数组下标?》。它不支持 mode,因为列表里的元素不是互斥地二选一。
.list-move 是列表独有的。插入或删除一项时,周围的项要挪位置,没有这条规则它们会瞬间跳过去;加上以后就会平滑移动,整个列表重新排序也一样。为什么还要给离开的项加 position: absolute?因为它要先脱离文档流,其他项才能算出新位置并开始移动。
常见追问
v-show 和 v-if 在 Transition 里有什么区别?
两者都能触发进入和离开动画。区别在动画结束后:
v-if会把元素移除,v-show只是设置display: none。另外@leave-cancelled钩子只在v-show下可用。
怎样让列表项一个接一个地出现?
官方示例的做法是给每项加
:data-index="index",用:css="false"和 JS 钩子,在onEnter里按el.dataset.index算出延迟,比如乘以 0.15 秒,动画完成时调done。
组件被 KeepAlive 缓存,又要切换动画,该怎么写?
把
<KeepAlive>放在<Transition>里面,让<component :is>作为 KeepAlive 的子元素。缓存本身的边界见《KeepAlive 与 Teleport 有哪些使用边界?》。