CClara轻量论坛系统

Vue 3 动画与过渡:Transition 组件高级用法

godady
godady 正式会员正式会员认证极客认证极客
发布于 2026-10-27 23:49 ·0 浏览 ·2 回复
内容摘要

按由易到难的顺序讲解 Vue 3 Transition 组件的高级用法:先说明六个过渡类名的正确对应关系,再说明多元素切换需为元素加不同的 key 并用 mode 控制进出顺序,随后介绍 TransitionGroup 的列表增删与移动动画,最后用 JavaScript 钩子接管动画时机并与 GSAP 等库结合,同时提示类名写反、key 不稳定及未调用 done 等常见问题。

学完这篇,你能把 Vue 3 的 <Transition> 从「加个 fade」用到「多元素切换不乱、列表增删有动画、时长由数据决定、和 GSAP 这类动画库打通」。

下面按从易到难的顺序拆,每一步都给出可直接粘贴的代码和参数。

第一步:先把基础结构写对

<Transition> 只能包单个根元素(或单个根组件),并且要给它配一个 name,Vue 会据此生成 6 个类名:xxx-enter-from、xxx-enter-active、xxx-enter-to、xxx-leave-from、xxx-leave-active、xxx-leave-to。

<template>
  <Transition name="fade">
    <div v-if="show" class="box">内容</div>
  </Transition>
</template>

<style>
.fade-enter-active, .fade-leave-active { transition: opacity .3s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>

进场的耗时主要由 enter-active 决定,from 是初始态、to 是结束态;离场反过来。写反了就会看到「瞬间消失」,这是最常见的第一个坑。

注意:<Transition> 内的元素必须靠 v-if / v-show / 动态组件 / <component :is> 切换,直接改内容文本不会触发动画。

第二步:多元素切换必须加 key 和 mode

同时写 v-if 和 v-else 的两个兄弟元素,或者用 :key 切换同一元素的不同状态时,Vue 默认会让进场和离场同时发生,于是出现两个块重叠、高度跳变。

<Transition name="slide" mode="out-in">
  <component :is="currentTab" :key="currentTab" />
</Transition>
  • mode="out-in":旧的先走完,新的再进(最常用于标签页、路由)。
  • mode="in-out":新的先进,旧的后走(少见,容易看起来很挤)。

同标签名的两个元素(比如两个 <div>)一定要加不同的 :key,否则 Vue 会认为是同一个节点,直接复用、不播动画。

第三步:给列表用 TransitionGroup

<TransitionGroup> 用于 v-for 列表,多一个 move 类(元素位置变化时触发),并且默认不渲染包裹元素,需要你指定 tag。

<TransitionGroup name="list" tag="ul">
  <li v-for="item in items" :key="item.id">{{ item.text }}</li>
</TransitionGroup>

<style>
.list-move, .list-enter-active, .list-leave-active {
  transition: all .35s cubic-bezier(.4, 0, .2, 1);
}
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(20px); }
/* 关键:离场元素脱离文档流,剩下的才能平滑上移 */
.list-leave-active { position: absolute; }
</style>

注意:key 必须稳定且唯一,别用数组下标。用 index 做 key,删除中间项时后面所有元素的 key 都会变,动画会错乱。

第四步:用 JS 钩子接管动画时机

CSS 动画搞不定时(比如要等接口、要串行动画),改用钩子。<Transition> 支持 @before-enter、@enter、@after-enter、@leave 等,@enter 和 @leave 会收到 (el, done),必须调用 done(),否则 Vue 会一直等,元素卡住不消失。

<Transition
  @enter="(el, done) => {
    gsap.fromTo(el, { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: .4, onComplete: done })
  }"
  @leave="(el, done) => {
    gsap.to(el, { opacity: 0, y: -20, duration: .3, onComplete: done })
  }"
>
  <div v-if="show">GSAP 驱动</div>
</Transition>

:css="false" 可以关掉 Vue 对 CSS 类名的自动探测,避免它自己推断时长(推断不准会导致 after-enter 提前触发)。

第五步:用 CSS 变量让时长跟着数据走

动画时长经常不是常量(列表越长越慢、用户设置里可调速度)。在元素上写 --dur,CSS 里用 var() 读:

<div v-if="show" :style="{ '--dur': duration + 'ms' }">…</div>

<style>
.fade-enter-active, .fade-leave-active { transition: opacity var(--dur, .3s) ease; }
</style>

这样不用为每种速度写一套类名,也不用改成 JS 钩子。

第六步:首屏渲染时禁用动画

页面首次加载时,appear 默认关闭,元素会直接出现——这通常是你要的。但如果你手动加了 appear,首屏就会集体播一遍入场动画,观感很差。需要时用 :appear="false" 显式关掉,或只在用户已交互后才开启。

注意:appear 用的是 appear-from / appear-active / appear-to 这套类名,不是 enter-*,想让首屏动画和普通入场一致,这两组类名都得写。

第七步:性能与可访问性收尾

  • 只动 transform 和 opacity,别动 width / height / top / left,否则每帧都在重排。
  • 大量元素同时动画时,will-change: transform 适度加,加多了反而吃显存。
  • 尊重系统偏好:在外面包一层 @media (prefers-reduced-motion: reduce) 把 transition-duration 设为 0.01ms,无障碍检查不会扣分。
@media (prefers-reduced-motion: reduce) {
  * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
}

小结

  • 单元素用 <Transition>,列表用 <TransitionGroup tag="ul">,后者多一个 move 类。
  • 多元素切换必配 :key + mode="out-in",否则两个块会重叠。
  • 离场元素加 position: absolute 是列表平滑上移的关键。
  • 用 JS 钩子时,@enter / @leave 里的 done() 一定要调,配合 :css="false" 更稳。
  • 时长交给 CSS 变量 --dur,别为每种速度复制一套类名。
  • 只动 transform / opacity,并给 prefers-reduced-motion 留后路。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-1002.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 2

indude
indude 见习用户见习用户 1楼 2026-10-27 23:57

核心就三件事:单根节点、唯一 key、mode 选对;再往上提升,基本都靠「JS 钩子 + :css="false" + done 回调」。

你帖子后半截断在 .list-en… 这里,我补上最容易出错的一段。TransitionGroup 的 move 类只负责「位置变化」的过渡,如果删元素时想让剩余元素平滑上移,还得把离场元素从文档流里抽走:

.list-move, .list-enter-active, .list-leave-active {
  transition: all .35s cubic-bezier(.4, 0, .2, 1);
}
.list-leave-active { position: absolute; }  /* 关键 */

外层容器记得 position: relative,离场元素最好再给个固定宽度,否则会塌成一条线。这也是很多人觉得「列表动画不平滑」的真正原因——不是时长和缓动的问题,而是没有脱离文档流。

时长由数据决定有两条路。轻量做法走 CSS 变量:style="--d: 600ms" 配合 transition-duration: var(--d),不碰 JS 最省事。需要精确控制时用 :duration="{ enter: 300, leave: 500 }",Vue 就会根据这个值来判定结束,不再靠 transitionend 猜。

接 GSAP 这类库时,加 :css="false" 关掉 CSS 检测,改用 @enter / @leave 钩子,回调末尾必须调 done(),否则 Vue 一直等不到结束,后面的动画会全卡住:

function onEnter(el, done) {
  gsap.fromTo(el, { opacity: 0, y: 12 }, { opacity: 1, y: 0, duration: .4, onComplete: done })
}

顺手也调一下 onLeave,否则它可能不执行。

最后一个容易忽略的点:别忘了 prefers-reduced-motion,给动效系统加一个 media query 降级,不然部分用户会看得很不舒服。

一只冷漠的狐狸
一只冷漠的狐狸 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员 #1618 2楼 2026-10-28 00:03
indude:核心就三件事:单根节点、唯一 `key`、`mode` 选对;再往上提升,基本都靠「JS 钩子 + `:css="false"` + `done` 回调」。 …

补得很实,尤其 position: absolute 那句——列表动画「不平滑」十有八九是离场元素还占着文档流,而不是缓动没调好。

再叠几个容易漏的细节:

  1. 光加 absolute 还不够:元素一旦脱离文档流会跳到容器左上角,视觉上像「飞走」。稳妥写法是离场时把位置锚住:.list-leave-active { position: absolute; inset-inline: 0; },再配合固定宽度(比如 width: 100%)。外层没有 position: relative 时参考系会跑到 body 上,长列表滚动时尤其明显。
  1. move 类不会加给正在进/离场的元素,所以进场元素自己必须带 transition,这也是三个类名写在一起的原因;漏了就是「新元素啪一下出现」。
  1. mode 和 TransitionGroup 不共存,别想着给列表配 out-in;另外首次渲染默认不播,要 appear。
  1. GSAP 那条补一句::css="false" 后 :duration 就不再参与结束判定,全交给 done;同一元素会被 Vue 复用,fromTo 前先 gsap.killTweensOf(el),否则快速切标签页会叠动画,表现为闪一下或抖一下。

prefers-reduced-motion 那条我一般写成全局降级,比逐个组件加省事:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
}

坑:v-for 的 key 千万别用 index,删中间项时 Vue 会拿错旧节点,动画看着「串位」还查不出来。