ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue 3过渡组件实战:Transition与TransitionGroup

Vue 3过渡组件实战:Transition与TransitionGroup Vue 3的过渡组件说实话刚在文档里看到时不少人会把它当成“正在开发中、还没稳定”的东西。毕竟Vue 2时代我们用得更多的是CSS动画加v-if判断或者干脆引一个Animate.css手动切换类名。但Vue 3的Transition和TransitionGroup其实已经是一套非常成熟、拿来即用的方案尤其是组合式API普及之后过渡逻辑可以写得更干净配合CSS变量或者JavaScript钩子能做到的动效远比想象中多。这篇文章我想从一个实际做项目的角度出发聊聊Vue 3里过渡组件到底该怎么用、底层是怎么跑的、项目里哪些场景适合用、哪些场景别硬用以及我踩过的一些坑。无论你是刚看完Vue 3文档准备上手还是在后台管理系统里想给列表加个增删动画又或者准备面试时被问到v-enter-from和Vue 2的v-enter到底有什么区别这篇应该都能帮到你。1. 过渡组件到底解决什么问题使用场景与方案选型1.1 从用户体验倒推为什么需要过渡动画你想想看一个页面里如果兄弟节点突然消失、弹窗突然出现、列表项瞬间被删掉用户第一反应往往是“我是不是点错了数据是不是有问题”——因为人眼对“突变”非常敏感但对“渐变”天然宽容。过渡动画的本质不是炫技而是给状态变化一个“缓冲解释”告诉用户这个东西正在被移走那个东西正在进来界面的逻辑是连续的。Vue 3的过渡组件解决的就是这个需求而且解决得很彻底。你只需要告诉Vue“我想让某个条件渲染的节点有动画”剩下的插入、移除、类名切换、时机控制它都帮你处理好。本质上它做了一件事在元素被插入或移除的瞬间给DOM元素挂上一系列预设的类名并严格按照时间线触发。而你的工作只是用CSS去定义这些类名对应的样式或者在JS钩子里写动画逻辑。1.2 Transition与TransitionGroup的职责边界如何划分很多人刚接触时容易混淆这两个组件其实分界线很清晰Transition负责单个元素/单个组件的进入和离开动画。典型场景是弹窗、抽屉、消息提示、条件渲染的区块。TransitionGroup负责列表中的多个元素在插入、移除、移动时的动画。典型场景是动态标签、任务列表、购物车明细。如果用一个生活化的类比Transition像是舞台上的单人表演演员进场退场有灯光跟随TransitionGroup像是合唱团人多了还要考虑前后排位置调整时怎么移动得更自然。另外要注意的是Vue 3里这两个组件都被设计成内置组件不需要额外注册也几乎不依赖第三方库就能跑起来。它内部的渲染逻辑用的是v-if配合Fragment处理所以TransitionGroup的子节点必须有唯一的key否则Vue没法判断哪个节点是新增的、哪个是移除的、哪个只是换了位置。1.3 什么场景不适合用过渡组件聊完优点也得泼点冷水。并不是所有动画场景都适合直接用内置过渡组件我总结了几种常见的不适配场景页面级别的路由切换动画路由切换通常涉及多个组件的并行进出而且需要和keep-alive配合直接在每个页面外用Transition包一层经常会有首屏渲染闪烁的问题。更推荐的做法是结合modeout-in做简化版的路由过渡或者直接使用vue-router的过渡钩子配合Transition但要做好缓存策略。高频、长时间循环动画比如转圈加载、反复弹跳的提示气泡。这类动画应该用CSSanimation的无限循环而不是反复切换v-if。用Transition去做无限循环会非常别扭而且会不断增加DOM插入移除的成本。多个独立、无关联的元素同时进出如果一个页面上有七八个互不相关的元素各自需要动画把它们各自包一层Transition没问题但如果你想统一调度它们的出场顺序、延迟时间Transition并不擅长编排。这时候用JS动画库比如GSAP的时间线会更合适。所以我的建议是先判断场景是“单元素进出”还是“列表变化”再判断是否需要精细编排最后决定用Transition、TransitionGroup还是第三方的JS动画库。2. Transition核心机制拆解类名时序、模式切换与JS钩子2.1 六个类名的完整时序别再只背面试题Vue 3的过渡类名一共有六个v-enter-from、v-enter-active、v-enter-to、v-leave-from、v-leave-active、v-leave-to。如果你给Transition加了namefade那么类名就变成fade-enter-from这种前缀形式。这个时序很关键我直接说一下进入动画的过程元素即将被插入时Vue立刻加上fade-enter-from和fade-enter-active。在下一帧也就是浏览器即将渲染前fade-enter-from被移除同时加上fade-enter-to。当CSS中定义的过渡或动画结束时fade-enter-active和fade-enter-to都会被移除。离开动画也差不多元素还留在DOM里时先加fade-leave-from和fade-leave-active下一帧加fade-leave-to等动画结束后整个元素才被移除。这里有一个容易踩的坑如果CSS里没有设置transition或animation类名切换是瞬间完成的元素会直接显示或消失看起来就像没动画一样。很多新手写了类名但忘了在enter-active或leave-active里指定过渡属性结果死活看不到效果。我实际项目中常用的一套淡入淡出CSS是这样的.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; } .fade-enter-from, .fade-leave-to { opacity: 0; }注意fade-leave-active要保留到动画结束所以transition必须写在leave-active里而不是写在leave-to里。2.2 mode属性in-out还是out-in选错会非常鬼畜在同一个Transition里如果新元素进入的同时旧元素离开默认情况下两者是同时进行的。这在某些视觉场景下会产生重叠看起来乱糟糟的尤其是两个元素尺寸差异很大的时候。modeout-in的意思是旧元素先完整离开新元素再进入。这个模式在弹窗关闭后打开另一个弹窗、路由切换时特别常用因为逻辑上“先走再进”符合直觉。modein-out则相反新元素先进来旧元素再走。听起来好像用得少但在某些场景很实用比如数字滚动替换、文字内容变更时新的内容先出现旧的内容淡出视觉上比较“沉稳”。我自己做弹窗通常用out-in而且强烈建议所有切换类动画都显式设置mode。如果你不设默认是同时进行碰到两个块级元素位置重叠时可能出现闪烁或者被遮挡的情况排查起来比想象中麻烦。你用绝对定位可以规避重叠但能用mode解决的问题没必要绕路。2.3 appear、name和自定义过渡类几个容易被忽略的参数appear这个属性经常被忽略。默认情况下Transition只在元素首次挂载时不会触发动画只有后续的插入和移除才有动画。如果你想在页面加载时元素就直接带上入场动画需要加上appear。name也不只是用来改类名前缀。它有一个很实用的点你可以在同一个页面里定义多套不同前缀的过渡CSS然后根据不同的组件状态动态切换name。比如列表筛选时用list-filter系列的动画弹窗用dialog系列的动画互不干扰。自定义过渡类名是enter-from-class、enter-active-class、enter-to-class、leave-from-class、leave-active-class、leave-to-class。这套机制主要是为了接第三方CSS动画库比如Animate.css。你可以写成Transition enter-active-classanimate__animated animate__fadeInUp leave-active-classanimate__animated animate__fadeOutDown div v-ifvisible内容/div /Transition用了这套接口之后name其实就不起作用了因为动画完全由你指定的类来驱动Vue只负责在正确的时机加类、去类。早年Vue 2的写法还需要自己管理进入和离开的classVue 3直接改成from、active、to三段式语义更清晰跟CSS过渡的四个状态也完全对上了。2.4 JS钩子当CSS搞不定时你还有完整的事件流有时候CSS过渡实现不了复杂动效比如要计算元素的初始坐标、做弹簧动画、控制动画时长和缓动曲线。Vue 3的Transition提供了八个钩子before-enter、enter、after-enter、enter-cancelled、before-leave、leave、after-leave、leave-cancelled。使用JS钩子时有两点必须注意在enter和leave钩子里动画结束必须调用done()回调否则Vue会认为动画永远没结束如果使用了JS钩子但没有在钩子中操作CSS属性最好给对应的enter-active和leave-active加上v-enter-active和v-leave-active类通过CSS把transition设置为none避免两者互相干扰。我举一个用GSAP做飞入动画的例子大概长这样Transition before-enterbeforeEnter enterenter leaveleave div v-ifvisible classcard.../div /Transitionfunction beforeEnter(el) { el.style.opacity 0 el.style.transform translateY(20px) } function enter(el, done) { gsap.to(el, { opacity: 1, y: 0, duration: 0.4, onComplete: done }) } function leave(el, done) { gsap.to(el, { opacity: 0, y: -20, duration: 0.3, onComplete: done }) }这样接动画库非常干净而且不需要在CSS里写任何过渡属性。如果你只是用CSS就行那不要用JS钩子省得多了不必要的复杂度。3. TransitionGroup列表过渡增删移动的动画实现3.1 列表增删的transition-group比想象中简单先看一个最基础的任务列表增删动画。数据源是一个数组我用v-for渲染成列表然后用TransitionGroup包住TransitionGroup namelist tagdiv div v-foritem in items :keyitem.id classtask-item {{ item.name }} button clickremoveItem(item.id)删除/button /div /TransitionGroupCSS部分.list-enter-active, .list-leave-active { transition: all 0.4s ease; } .list-enter-from { opacity: 0; transform: translateY(30px); } .list-leave-to { opacity: 0; transform: translateX(30px); }这个写法跟单个Transition的类名几乎一样区别在于每个子元素都会独立触发自己的进入或离开动画而且TransitionGroup默认渲染成一个真实的元素tag属性可以指定渲染成ul、div或者别的标签。这里有一个非常关键的坑删除元素时不要用v-if去控制而是直接从数据数组里filter掉。因为TransitionGroup是根据key来识别节点存续的如果你用v-if把DOM摘掉Vue根本不知道这是“离开”还是“直接消失”动画自然就没了。3.2 FLIP为什么列表项位置变化也能自动动画TransitionGroup最让人惊喜的是它能自动处理列表项移动时的位移动画。这背后用到的技术叫FLIP即First、Last、Invert、Play。简单解释一下这个过程动画开始前Vue记录每个元素的初始位置First数据变化导致DOM更新后元素到了新位置Last然后Vue计算位置差通过transform把元素先“拉回”原位Invert最后让transform过渡到0元素就从旧位置平滑移动到新位置Play。这意味着你在做拖拽排序、插队、筛选重排这些操作时不需要手动计算任何坐标TransitionGroup会帮你把这部分动画补上。实际体验非常顺滑而且性能上因为只操作transform和opacity基本不会引起大面积重排。为了触发移动动画你只需要保证两点数据顺序变化后v-for的渲染顺序同步变化每个元素有稳定且唯一的key。如果key用的是数组索引插入中间项时后面的元素会全部被当成新元素重建移动动画就会失效还会造成奇怪的视觉闪烁。3.3 实战购物车加减数量与拖拽排序的动画细节我这里分享一个我做过的购物车场景。购物车明细就是典型的TransitionGroup增删商品项要有进出动画数量加减时商品排序可能变化需要移动动画。关键代码逻辑是每次操作数据后列表重新渲染TransitionGroup namecart tagdiv classcart-list div v-foritem in cartList :keyitem.skuId classcart-item span{{ item.name }}/span button clickchangeQty(item.skuId, 1)/button button clickchangeQty(item.skuId, -1)-/button button clickremoveItem(item.skuId)移除/button /div /TransitionGroupCSS里给每个商品项加上transition并在离开时使用绝对定位避免后面的元素跳动。这里有个小技巧给.cart-leave-active加上position: absolute可以让离开的元素脱离文档流其他元素补位时使用FLIP移动动画比直接塌陷要平滑很多。我踩过的坑是离开动画时元素脱离文档流后列表容器的高度会突然变小如果容器有固定高度会露出空白。后来我把容器的最小高度设置成跟一个商品项高度一致视觉上就基本看不出来了。4. 常见问题与排查技巧实录4.1 页面刷新初始化时元素没动画这是被问得最多的一个问题。原因前面提过Transition默认不渲染初始动画。解决办法是加appear属性。注意加appear之后你写在enter-active里的动画会在首次渲染时也执行一次。如果页面里有多个区块都用Transition加了appear它们会同时进场层次感会差一些。想要错峰入场可以给每个元素加上不同的transition-delay比如第一个延迟0ms、第二个延迟100ms。4.2 动态组件切换动画没生效或错位用component :iscurrentComponent做动态组件切换时通常需要这样包一层Transition namefade modeout-in component :iscurrentComponent / /Transition这里最容易踩的坑是多个组件没有根元素或者根元素类型不同导致Vue无法复用同一个DOM节点动画在切换时直接跳过。解决方法是给每个子组件或最外层包一层相同标签的元素或者给component外层的Transition里的节点设置一个不变的key。如果是组件内有多根节点可以考虑给组件加inheritAttrs之类的属性但最简单的还是包一层。4.3 同时使用CSS过渡和JS动画状态互相打架如果你在enter-active里写了CSS的transition又在enter钩子里用GSAP控制动画两边都会修改transform和opacity结果就是动画混乱结束时间不可控。我的经验是一个过渡只选一条路。用CSS就专心写CSS类名用JS钩子就把CSS过渡关掉比如.js-anim-enter-active, .js-anim-leave-active { transition: none !important; }这样至少能保证JS钩子里的done()回调时机准确不会因为CSS还在过渡而被提前触发或者被延迟。4.4 性能问题文档流大面积变化时动画卡顿列表项很多加上移动动画一帧内要做很多布局计算低端设备上会卡。常见的优化手段是动画属性尽量只用transform和opacity避免动画过程中触发布局layout和绘制paint开销大的属性比如width、height、top、left。对列表容器加上will-change: transform或will-change: opacity主动告诉浏览器提前优化但别滥用否则内存占用会变大。大量列表项的进入动画如果同时涌入可以用transition-delay做分帧入场把一帧的密集计算拆散到多个帧里。实测下来超过200个列表项的移动动画在普通电脑上也能保持流畅前提是别在动画过程里大量修改数据也别在动画期间频繁触发响应式更新。4.5 面试点延伸Vue 2和Vue 3的过渡类名差异准备面试的话过渡组件的类名差异是个很经典的问题。Vue 2用的是v-enter、v-enter-to、v-enter-activeVue 3把进入起始状态改成了v-enter-from语义上更对称enter-from对应enter-toleave-from对应leave-to。另外Vue 3里Transition和TransitionGroup都支持了appear而且JS钩子的命名从beforeAppear这类变成了直接复用before-enter等钩子API更统一。如果你之前用Vue 2写过自定义过渡迁移到Vue 3时大部分改动其实只发生在类名和钩子的命名上逻辑本身不用大改。5. 过渡组件在真实项目中的扩展玩法5.1 给Element Plus的弹窗接上更细腻的过渡后台管理系统里用Element Plus比较多Dialog和Drawer都有内置动画但有时候需要定制。Element Plus本身提供了transition相关的属性和overlay插槽可以在不破坏组件结构的情况下用自定义Transition去覆盖默认行为。比如想做成从右下角弹入的“通知抽屉”我可以利用Transition的JS钩子读取元素的尺寸和位置动态计算起始坐标再用transform做平移。这种玩法比单纯改CSS灵活得多。5.2 用TransitionGroup实现拖拽排序的视觉反馈拖拽排序如果只靠第三方拖拽库放下时经常“啪”一下就位了很生硬。把列表包进TransitionGroup之后拖拽过程中你只需要实时更新数组顺序其余节点会自动用FLIP动画让位整套交互会高级很多。这也是我在实际项目里最喜欢的用法所有排序逻辑归你视觉反馈归Vue。5.3 结合Teleport把弹窗动画放到body下Vue 3的Teleport经常和Transition配合。弹窗通常要挂载到body下避免被父容器overflow: hidden裁剪但Transition要求动画的进出条件跟同一个节点绑定。正确的姿势是把Teleport和Transition叠着用Teleport tobody Transition namemodal div v-ifvisible classmodal-mask ... /div /Transition /Teleport这样既能避免样式污染又能让遮罩和弹窗层的进出动画正常触发。需要注意如果项目里用了样式隔离方案比如scoped:deep()选择器要覆盖到挂载后的目标节点上不然动画类名的样式在body下找不到。5.4 给“正在开发中的过渡组件”正名回到标题这个说法。Vue 3的过渡组件在文档里一直存在但从使用者的角度看它确实是随着组合式API和生态逐步完善而被更多人关注到的“活跃组件”。尤其在新版Vue Router和Pinia成为标配后很多人开始重新审视过渡动画给后台管理系统带来的价值。如果你刚接触不必等什么“正式版”或者“稳定版”直接上手就好这个组件本身已经很稳该踩的坑我上面也都替各位踩过了。我个人在实际操作中的体会是过渡组件最大的价值不在于让页面“花里胡哨”而在于让每一次状态变化都有解释。列表增删、弹窗开关、筛选切换这些散落在业务代码里的琐碎交互加上合适的过渡之后整体质感会提升一大截。最后再提醒一句动画时长建议控制在200ms到400ms之间太短没有存在感太长用户会觉得拖沓这套参数在绝大多数业务场景里都成立。
返回列表