Vue3的Transition到底怎么用?能解决哪些常见的前端动效痛点?
刚上手Vue3动效开发的朋友,大概率都听过Transition组件的大名——毕竟它是Vue官方自带的、专门处理单元素/单组件进入离开过渡的“轻量级武器”,不用额外安装复杂的库,短短几行代码就能把生硬的切换变得丝滑,不过真到写的时候,可能又会遇到一堆问题:比如为什么动效不生效?Enter和Leave类名什么时候触发?怎么控制过渡时长?结合Teleport、Keep-Alive这些组件时要注意什么?还有列表过渡和它有啥区别?今天就用大家能听懂的大白话,把Transition从头到尾、从基础到进阶讲得明明白白。
Vue3 Transition的核心逻辑是什么?
很多人直接套类名写动效,但完全没搞懂组件内部是怎么运作的,结果一遇到问题就抓瞎,其实Transition的逻辑特别简单,本质上就是在元素/组件的「进入/离开生命周期节点」自动添加、移除CSS类名,或者触发对应的JavaScript钩子函数——你只要把这些节点对应的类名/钩子写好,剩下的切换时机Vue全帮你搞定,连DOM的操作顺序都给你安排得明明白白(比如Enter是先加类名,再等动画结束后移除;Leave是先加类名,等动画结束后再从DOM里删掉)。
为了更直观,举个最基础的切换小例子:你用v-if控制一个小方块的显示隐藏,外面套个Transition组件,取个名字比如“fade”——这时候Enter和Leave就会各有3个关键节点:
- 进入阶段的“入场准备期”(vue3里是fade-enter-from,旧版是fade-enter):元素刚被添加到DOM但还没显示,这时候你可以写初始的隐藏状态,比如opacity:0;
- 进入阶段的“入场激活期”(vue3里是fade-enter-active,旧版是fade-enter-active):这个节点会贯穿整个入场动画的全过程,也是你写transition/animation属性的地方;
- 进入阶段的“入场完成期”(vue3里是fade-enter-to,旧版是fade-enter-to,一般可以省略,因为激活期结束会自动把元素切到正常显示状态);
- 离开阶段和进入阶段是镜像的,对应fade-leave-from、fade-leave-active、fade-leave-to,激活期同样用来写动效,完成期后Vue会把元素从DOM移除。
这里要提一句,很多新手一开始会把激活期的transition写在普通样式里——不是不行,但更规范的做法是单独写在-enter-active/-leave-active里,因为只有在切换阶段才需要激活过渡,平时切换完就自动移除了,不占额外的样式权重,也更符合组件的封装逻辑。
为什么我写的Vue3 Transition动效完全不生效?
这绝对是被问得最多的问题,没有之一,别慌,99%的情况下都是以下4个原因之一,一个一个排查就能解决:
Transition包裹的元素/组件没有触发“进入/离开”的条件
Transition只认两种切换方式:要么是v-if/v-else/v-else-if控制的条件渲染,要么是v-show控制的条件显示,要么是key值改变导致的组件强制替换(这个进阶里会讲),如果你的元素只是用display:none/block手动切换,或者绑定的是普通的class/style控制透明度位移——对不起,Transition根本感知不到,自然不会加类名。
比如很多新手会犯的错:把v-if放在Transition外面了,比如
还要注意,Transition只能包裹单个直接子元素,或者用条件渲染/key值切换的单个备选子元素——如果你同时放了两个没有v-if/v-else绑定的div,Vue会报错的,这时候要用TransitionGroup来处理,后面会简单对比下两者的区别。
没有正确写CSS类名的前缀和版本
前面说了,Vue3的类名前缀默认是v,你可以通过Transition的name属性改成自己想要的,比如前面的fade;而且Vue3的类名和旧版Vue2有个小小的区别:旧版是enter/leave,新版统一加了-from,不然动效也出不来。
很多时候大家直接复制了Vue2的代码到Vue3,就会发现只有离开的动效生效(或者反过来?其实要看具体情况,比如你写的是旧版的fade-enter,但新版只有fade-enter-active被正确应用,可能出现跳变但不流畅的情况)——这时候只要把-from加上就行,或者在Vue3的项目里配置兼容旧版的类名?不过没必要,直接改更符合Vue3的规范。
动效的时长和Vue的判断不匹配
Transition组件默认会自动监听直接子元素的第一个transitionend或animationend事件,然后自动移除类名、完成DOM操作——但如果你写的是多个层级的嵌套动效(比如方块移动的同时里面的文字在淡入),或者写的transition/animation有延迟,Vue可能会等第一个动效结束就提前操作了,导致后面的动效被打断。
这时候有两个解决办法:要么通过Transition的duration属性手动指定总时长(单位是毫秒,duration="1000"就是所有动效都等1秒),要么用JavaScript钩子函数自己控制结束时机(后面会讲)。
还有一种反向的情况:你忘记给动效写时长了!比如只写了opacity:0和opacity:1,但没写transition:opacity 0.5s——这时候元素只是瞬间切换,根本没有过渡效果,自然看起来像没生效一样。
直接子元素的样式被覆盖了
有时候直接子元素有自己的display属性,或者有更高级的样式权重,比如用!important或者内联样式——这时候Transition加的类名可能会被覆盖,导致动效不生效。
比如你写了
这时候要么调整样式权重,要么在动效类名里也用!important(不过尽量少用),要么把内联样式改成普通的class绑定。
Vue3 Transition除了写CSS类名,还有没有别的玩法?
当然有!CSS类名是最常用的,但遇到一些复杂的动效(比如需要结合TweenLite/TweenMax等第三方库,或者需要操作DOM的属性、做一些计算逻辑的动效),就可以用Transition自带的JavaScript钩子函数。
钩子函数都是通过Transition的属性绑定的,before-enter、@enter、@after-enter、@enter-cancelled、@before-leave、@leave、@after-leave、@leave-cancelled,一共8个——每个钩子的第一个参数都是el,也就是当前Transition包裹的直接子元素的DOM对象,你可以直接操作它;第二个参数是done函数,只有当你不用CSS类名,完全用JS写动效的时候才需要调用done,不然Vue会自动等动效结束;第三个参数只有在组件强制替换的时候才会有,是旧的DOM元素。
举个用第三方库GSAP写的例子(GSAP是现在前端最火的动效库之一,比原生transition/animation灵活很多):你要让一个小方块从左边滑进来,然后旋转180度,最后放大1.2倍,离开的时候反过来——这时候用GSAP结合钩子函数就特别简单: 首先引入GSAP,然后给Transition绑定@enter和@leave钩子:
<template>
<transition @enter="enterHandler" @leave="leaveHandler" :css="false">
<div v-if="show" class="box"></div>
</transition>
</template>
<script setup>
import { ref } from 'vue'
import { gsap } from 'gsap'
const show = ref(false)
const enterHandler = (el, done) => {
gsap.fromTo(
el,
{ x: -200, rotation: 0, scale: 0.8, opacity: 0 },
{ x: 0, rotation: 180, scale: 1.2, opacity: 1, duration: 0.8, ease: 'back.out(1.7)', onComplete: done }
)
}
const leaveHandler = (el, done) => {
gsap.to(
el,
{ x: 200, rotation: 0, scale: 0.8, opacity: 0, duration: 0.5, ease: 'power2.in', onComplete: done }
)
}
</script>
这里要注意两个点:一是要给Transition加:css="false",告诉Vue“不要用CSS类名的逻辑,完全听我JS的”,不然Vue可能会同时触发CSS和JS的逻辑,导致冲突;二是在GSAP的onComplete回调里调用done函数,告诉Vue“我的动效做完了,你可以继续操作DOM了”——如果不调用done,Vue会一直等下去,比如离开的时候元素会一直留在DOM里不动。
除了GSAP,你还可以用原生的Web Animations API(WAAPI),这个API现在的浏览器兼容性已经很好了,Chrome、Firefox、Safari 13.1+、Edge 79+都支持,不用引入任何第三方库,性能也比JS手动操作样式好很多,因为它是浏览器原生支持的,走的是GPU加速的路径(前提是你操作的是transform、opacity这些不会触发重排的属性)。
Vue3 Transition结合Teleport、Keep-Alive这些组件时要注意什么?
现在Vue3的很多项目都会用到Teleport和Keep-Alive,和Transition结合的时候有几个小坑,提前踩过就不会浪费时间了:
结合Teleport的坑
Teleport是用来把元素/组件“传送”到DOM树的其他位置的,比如弹窗、提示框——这时候Transition一定要和Teleport一起套在要传送的元素外面,顺序是:先Teleport,再Transition,或者先Transition再Teleport?不对,正确的顺序应该是把Transition放在Teleport里面,包裹要传送的元素——因为如果Transition在外面,Teleport会把元素移走,Transition感知不到DOM的位置变化,可能会导致动效不生效,或者离开的时候元素直接消失,没有过渡。
举个正确的弹窗例子:
<template>
<button @click="showModal = true">打开弹窗</button>
<Teleport to="body">
<transition name="modal">
<div v-if="showModal" class="modal-mask">
<div class="modal-content">
<h2>这是一个弹窗</h2>
<button @click="showModal = false">关闭弹窗</button>
</div>
</div>
</transition>
</Teleport>
</template>
这里的transition是直接包裹modal-mask的,放在Teleport的to="body"内部,这样进入离开的时候Vue都能正确感知到,动效也能正常生效。
还要注意,modal-mask一般要写position:fixed覆盖整个页面,这时候modal-content的动效可以单独写,比如从上面滑下来,而modal-mask的动效是淡入淡出——这时候modal-content可以单独再套一个Transition吗?可以,但其实可以直接用嵌套的类名控制,更简单。
结合Keep-Alive的坑
Keep-Alive是用来缓存组件状态的,比如Tab切换的时候,不用每次都重新渲染组件,直接复用缓存的状态——这时候要注意,Transition包裹的不是单独的组件,而是Keep-Alive包裹的动态组件或者v-if/v-else切换的缓存组件。
和单独用Transition不同的是,Keep-Alive缓存的组件“离开”的时候不会被从DOM里删除,而是被隐藏(display:none),进入”的时候再显示——这时候Transition的钩子函数里的el会是同一个DOM对象,而不是新的;而且类名的触发时机也有一点点变化,不过CSS类名的写法是一样的。
举个Tab切换结合Keep-Alive和Transition的例子:
<template>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
@click="currentTab = tab.id"
:class="{ active: currentTab === tab.id }"
>
{{ tab.name }}
</button>
<transition name="slide" mode="out-in">
<keep-alive>
<component :is="currentTabComponent" :key="currentTab" />
</keep-alive>
</transition>
</div>
</template>
这里要注意一个非常重要的属性:mode="out-in"——这个属性是用来控制两个切换的元素/组件的“出场顺序”的,默认是“in-out”,也就是新的先进入,旧的再离开;“out-in”是旧的先离开,新的再进入;还有一个“default”,也就是两个同时切换。
如果不用mode属性,Tab切换的时候两个组件会同时在DOM里,可能会出现重叠的情况,尤其是滑动动效的时候,特别明显——所以一般Tab切换、路由切换(后面会讲)都会用mode="out-in",看起来更丝滑。
还要注意,component的:key属性一定要加,而且要绑定和切换相关的变量(比如这里的currentTab)——不然Keep-Alive可能不会正确缓存或者替换组件,Transition也感知不到切换。
Vue3 Transition和TransitionGroup有什么区别?分别用在什么场景?
很多人会把这两个搞混,其实区别特别明显:
- Transition只能处理单个直接子元素/组件的进入离开过渡,或者用条件渲染/key值切换的单个备选子元素/组件;
- TransitionGroup只能处理多个同级子元素/组件的进入离开移动过渡,每个子元素/组件都必须有唯一的:key属性。
TransitionGroup其实是Transition的“增强版”,专门用来处理列表的,比如TodoList里添加、删除、修改Todo项的时候的动效——它除了有和Transition一样的Enter/Leave类名/钩子,还有一个Move类名/钩子,用来处理列表项因为其他项的添加/删除而产生的位置移动过渡。
举个简单的TodoList例子:
<template>
<div class="todo-list">
<input v-model="newTodo" placeholder="输入新的待办事项" @keyup.enter="addTodo">
<button @click="addTodo">添加</button>
<transition-group name="todo" tag="ul">
<li v-for="(todo, index) in todos" :key="todo.id">
{{ todo.text }}
<button @click="removeTodo(index)">删除</button>
</li>
</transition-group>
</div>
</template>
<script setup>
import { ref } from 'vue'
const newTodo = ref('')
let todoId = 0
const todos = ref([
{ id: todoId++, text: '买牛奶' },
{ id: todoId++, text: '写代码' },
{ id: todoId++, text: '运动' }
])
const addTodo = () => {
if (newTodo.value.trim()) {
todos.value.push({ id: todoId++, text: newTodo.value.trim() })
newTodo.value = ''
}
}
const removeTodo = (index) => {
todos.value.splice(index, 1)
}
</script>
<style scoped>
.todo-enter-active, .todo-leave-active {
transition: all 0.5s ease;
}
.todo-enter-from, .todo-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* 这个是处理移动过渡的关键类名 */
.todo-move {
transition: transform 0.5s ease;
}
/* 这个是处理离开时列表项位置变化的关键属性 */
.todo-leave-active {
position: absolute;
}
</style>
这里要注意三个点:
一是TransitionGroup的tag属性——默认情况下,TransitionGroup会渲染成一个元素包裹所有的子元素,你可以通过tag属性改成你想要的标签,比如这里的;
二是每个子元素都必须有唯一的:key属性——不能用index作为key,因为如果删除中间的项,后面的项的index会变,导致TransitionGroup识别错误,动效混乱,最好用每个项的唯一ID;
三是处理离开时列表项位置变化的两个关键点:一是要加.todo-move类名,用来写移动过渡的属性;二是要给.todo-leave-active加position:absolute——因为离开的项会先从DOM的正常流里脱离,其他项才能正确地移动到它原来的位置,不然其他项会等离开的项完全消失后才突然跳过去,没有过渡效果。
Vue3 Transition能用来做路由过渡吗?怎么做?
当然可以!这是Transition非常常用的一个场景——现在的单页应用(SPA)的路由切换都是没有刷新的,很生硬,加上Transition之后体验会好很多。
Vue3的路由过渡其实就是把Transition包裹在router-view组件外面,router-view其实就是一个动态组件,会根据当前的路由自动渲染对应的页面组件——所以和前面讲的Tab切换结合Keep-Alive和Transition的逻辑是一样的。
首先要确保你的项目已经安装并配置了Vue Router 4.x(因为Vue3必须用Vue Router 4.x,旧版的3.x不兼容),然后在App.vue里把router-view包裹在Transition里:
<template>
<div class="app">
<nav>
<router-link to="/">首页</router-link>
<router-link to="/about">lt;/router-link>
<router-link to="/contact">联系</router-link>
</nav>
<transition name="slide" mode="out-in">
<router-view v-slot="{ Component, route }">
<keep-alive :include="['Home']">
<component :is="Component" :key="route.path" />
</keep-alive>
</router-view>
</transition>
</div>
</template>
这里用了router-view的v-slot语法,这是Vue Router 4.x推荐的写法,用来获取当前的Component和route对象;然后用keep-alive的:include属性指定哪些组件需要缓存,比如这里的Home组件;然后给component加:key="route.path",确保路由变化的时候Transition能感知到切换。
还要注意,不同的路由可以有不同的动效——比如首页到关于用左滑,关于到联系用右滑,这时候可以通过route.meta属性来指定每个路由的动效名称: 首先在路由配置里给每个路由加meta:
const routes = [
{
path: '/',
name: 'Home',
component: () => import('./views/Home.vue'),
meta: { transition: 'fade' }
},
{
path: '/about',
name: 'About',
component: () => import('./views/About.vue'),
meta: { transition: 'slide-left' }
},
{
path: '/contact',
name: 'Contact',
component: () => import('./views/Contact.vue'),
meta: { transition: 'slide-right' }
}
]
然后在App.vue里用watch监听route对象的变化,根据前后路由的meta来动态设置Transition的name属性:
<template>
<div class="app">
<nav>
<router-link to="/">首页</router-link>
<router-link to="/about">lt;/router-link>
<router-link to="/contact">联系</router-link>
</nav>
<transition :name="transitionName" mode="out-in">
<router-view v-slot="{ Component, route }">
<keep-alive :include="['Home']">
<component :is="Component" :key="route.path" />
</keep-alive>
</router-view>
</transition>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const transitionName = ref('fade')
// 监听路由变化,动态设置过渡名称
watch(
() => [route.meta.transition, route.path],
([newTransition, newPath], [oldTransition, oldPath]) => {
// 这里可以根据前后路由的关系来设置,比如新路由的path比旧路由的长就是进入下一级,用左滑,反之用右滑
// 也可以直接用前后路由的meta.transition
// 举个简单的例子:如果新路由有meta.transition就用它,不然用默认的fade
transitionName.value = newTransition || 'fade'
}
)
</script>
这样不同的路由就能有不同的动效了,体验会更加个性化。
最后给刚上手Vue3 Transition的朋友几个实用的小建议
- 先从简单的动效开始练手:比如淡入淡出、滑动、缩放,不要一开始就写很复杂的3D动效或者嵌套多层的动效,等熟悉了类名的触发时机和核心逻辑之后再慢慢进阶。
- 尽量使用transform和opacity来写动效:这两个属性不会触发浏览器的重排(Reflow)和重绘(Repaint),而是走GPU加速的路径,性能会非常好,不会出现卡顿的情况——尽量不要用margin、padding、width、height这些会触发重排的属性来写动效。
- 注意浏览器的兼容性:虽然Vue3本身只支持现代浏览器(Chrome、Firefox、Safari 12+、Edge 79+),但如果你的项目需要兼容更老的浏览器,比如IE11,那就要用Vue2了,而且要给transform、opacity、transition这些属性加前缀(webkit-、-moz-、-ms-)——不过现在大部分项目都不需要兼容IE11了,所以不用太担心。
- 善用浏览器的开发者工具调试动效:Chrome的Elements面板里可以实时查看Transition添加/移除的类名,Animation面板里可以查看动效的时长、延迟、缓动函数等信息,还可以暂停、放慢动效,非常方便调试。
- 不要滥用动效:动效是用来提升用户体验的,不是用来炫技的——如果动效太复杂、太花哨,反而会让用户分心,甚至反感——所以一定要适度,比如提示框的淡入淡出、列表项的滑动、Tab切换的淡入淡出就足够了。
Vue3的Transition是一个非常强大、非常易用的动效组件,只要掌握了它的核心逻辑和常见的坑,就能用短短几行代码写出丝滑的前端动效,提升你的项目的用户体验。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


