前端知库

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 类,让其他项在列表变化时平滑移动,而不是跳过去。

六个类名分别在什么时候出现

先看最常见的淡入淡出:

vue
<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" 让旧元素先离开,离开完再插入新元素,切换组件时最常用:

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

<Transition> 里一次只能有一个元素或组件,组件也必须只有一个根元素。v-if、v-else 切换没问题,因为同一时刻只显示一个。

还有一个容易漏的点:数字变化想要动画,只改文字是不会触发的,因为 Vue 只更新了文本节点,元素没换。给元素加上 :key="count",每次值变了就会生成新元素,才有进入和离开可言。

首次渲染默认没有动画,需要的话加 appear。

用 JS 钩子写动画

用 GSAP 这类 JS 动画库时,用钩子拿到元素自己控制:

vue
<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、类名和钩子,但有几处区别:

vue
<TransitionGroup name="list" tag="ul">
  <li v-for="item in items" :key="item">{{ item }}</li>
</TransitionGroup>
CSS
.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 有哪些使用边界?》。