1. 先搞清楚Transition在解决什么:一个元素从"有"到"无"的过程
1.1 没有Transition时,v-if和v-show的微妙差别
在Vue3里我们最常用的两种显隐方式是v-if和v-show。表面上看效果差不多,但底层的差别直接影响动画方案。
v-if是真正地创建和销毁DOM节点。当条件为false时,元素直接从文档里消失,你没有任何机会在它消失前播放一段离场动画。v-show则只是切换display属性,元素始终在DOM树上,但display的切换也不参与CSS过渡——display不是一个可过渡的属性,你不可能写display从block过渡到none,浏览器压根不认这个。
于是大家惯用的做法就变成了这样:先把元素通过opacity和transition降到0,再setTimeout几百毫秒后真正销毁。代码写起来很别扭,而且定时器的时间一旦和CSS的transition时间不同步,动画就会闪烁或者突然消失。这个问题在页面切换场景下尤其明显——你做了一个后台管理系统,菜单从A页面切到B页面,如果A页面在销毁前没有完整的退场过程,视觉上就是"啪"地一闪,过渡感全无。
1.2 Transition组件像一个舞台灯光师
Vue3内置的Transition组件解决的正是这个问题。它不需要你手动管理setTimeout,也不需要你在元素上挂一堆状态类名,它做的是三件事:
- 感知元素的创建与销毁时机
- 在合适的时机自动添加/移除约定的类名
- 在离开动画完全结束后,再把元素从DOM中移除
这个过程可以拿舞台灯光来类比。元素就像一个演员,v-if把演员请上台或者送下台,但演员上台前灯光师要先把光打好,演员下台时灯光要跟着追一段,最后落幕。Transition组件就是那个灯光师,它知道什么时候该开灯、什么时候该关灯。
1.3 Vue2到Vue3:过渡类名变化带来的思路调整
如果你是从Vue2迁移过来的,会发现Vue3的过渡类名体系有一个明显变化。Vue2时代,进入过渡的开始状态叫v-enter,结束状态叫v-leave-to,命名并不对称。Vue3统一成了完整的三段式:enter-from、enter-to和leave-from、leave-to。换句话说,进入和离开都拥有了"开始状态、激活状态、结束状态"三件套,语义更对称,也更贴近CSS动画的本质。
这个变化不是单纯的改名,它意味着Vue3里你可以更精确地控制进入/离开每一帧的状态。比如进入动画从 opacity: 0 开始、转成 opacity: 1,离开动画从 opacity: 1 开始、转成 opacity: 0,四个类名各司其职,不容易混淆。网上很多老教程还停留在Vue2的 v-enter 写法,直接搬到Vue3里会发现动画完全不生效,这一点要格外注意。
2. 六个类名出现的时机与触发过渡的三种操作
2.1 六个类名分别在哪一帧出现和消失
Transition组件默认的类名前缀是 v-,如果你给组件设置了 name,前缀就改为对应的名字。以 name="fade" 为例,整个进入过程是这样的:
| 类名 | 添加时机 | 移除时机 | 典型用途 |
|---|---|---|---|
| fade-enter-from | 元素插入前 | 插入后的下一帧 | 定义进入起始状态,比如 opacity: 0 |
| fade-enter-active | 元素插入前 | 过渡或动画结束 | 定义整个进入过程的过渡属性、时长、曲线 |
| fade-enter-to | 插入后的下一帧 | 过渡或动画结束 | 定义进入结束状态,比如 opacity: 1 |
| fade-leave-from | 离开触发时 | 离开的下一帧 | 定义离开起始状态 |
| fade-leave-active | 离开触发时 | 过渡或动画结束 | 定义整个离开过程的过渡属性、时长、曲线 |
| fade-leave-to | 离开触发的下一帧 | 过渡或动画结束之后移除DOM | 定义离开结束状态 |
这里值得展开的是"下一帧"这个词的含义。为什么 enter-from 要在插入后的下一帧移除?因为浏览器必须先渲染出 enter-from 定义的初始画面,然后才能在下一个渲染帧里应用 enter-to 的目标状态,并且通过 enter-active 上的 transition 属性让变化过程平滑发生。如果这两个类在同一帧里切换,浏览器会直接跳到最后状态,动画就消失了。
2.2 什么操作能触发过渡:v-if、v-show、动态组件
Transition组件能感知三类切换场景:
- v-if 控制元素创建与销毁
- v-show 控制元素的显示与隐藏(注意,v-show 在元素隐藏时由于 display:none 和外层过渡同时发生,实际效果经常需要额外处理,这个在第7部分会细说)
- 动态组件 <component :is="..."> 切换
列表场景(v-for)通常要使用 TransitionGroup,而不是 Transition。原因很简单:Transition 关心的是单个节点的"进场/退场",而 TransitionGroup 还要额外处理列表项的插入、移除和移动时的位置变化。
组件切换能触发过渡这一点对路由动画很关键。路由切换本质上就是 router-view 根据当前路由渲染不同的页面组件,所以 Transition 天然适合跟路由联动。
2.3 初始进入动画为什么需要appear
默认情况下,Transition 只会在条件发生变化时触发过渡,组件首次挂载时不会播放动画。但页面切换是一个特殊场景——用户从登录页跳到首页,首页的第一屏如果也想做一个淡入效果,就需要给 Transition 加上 appear 属性。加了 appear 之后,元素初始挂载时也会走一遍 enter 系列的类名流程,相当于"首次亮相也打光"。
类似地,如果你定义的是自定义类名,appear 阶段也有对应的 appear-active-class、appear-from-class 和 appear-to-class 可以单独指定。大多数页面切换场景直接用默认的 enter 类名就够,appear 类名用得比较克制,但确实是一个绕不开的开关。
3. 路由切换实战:Transition与router-view插槽的正确组合方式
3.1 官方推荐的结构:v-slot解构Component
在Vue Router 4里,直接像Vue2那样把 <transition> 包在 <router-view> 外面会收到一个警告:<router-view> can no longer be used directly inside <transition> or <keep-alive>。原因是路由组件可能是异步加载的,Vue Router 4 需要把实际渲染的Component对象暴露出来,由你来决定如何渲染。
正确的写法是利用 router-view 的插槽能力:
<template> <router-view v-slot="{ Component }"> <transition name="fade" mode="out-in"> <component :is="Component" :key="$route.path" /> </transition> </router-view> </template>这里 <component :is="Component"> 才是真正的页面组件,transition 只需要包住它,就可以捕获到组件切换时的进入/离开时机。模板里 "$route" 是全局路由对象,如果你在组合式API里更习惯用 useRoute,也可以显式声明。
3.2 不给组件加key,过渡可能不生效或页面不刷新
很多人第一次写到这里会疑惑:<component :is="Component"> 明明绑定的是当前路由组件,为什么还要再绑一个key?
原因在于Vue的渲染复用策略。当两个路由配置的组件类型不同时,Vue能识别出组件变了,会正常触发过渡;但当你从 /user/1 切到 /user/2,也就是同一个组件在不同参数下切换时,Vue默认会复用这个组件实例,不会重新走创建/销毁流程,过渡自然就不会触发。
更麻烦的是,不少项目里的页面组件虽然路径不同,但复用的是同一个基础组件(比如一个通用列表页,通过不同路由参数显示不同数据)。这种情况下如果不加 key,页面甚至不会重新拉数据,因为组件实例没有重建。所以我的习惯是:只要在路由切换中需要保证"每次路由变化都重新挂载",就统一给组件加一个跟路由强关联的key。
key 的选择也有讲究:
- :key="$route.path" 只在路径变化时重建组件,query 变化不会触发
- :key="$route.fullPath" 会把 query 也纳入,query 变化也会重建并重新触发过渡
- :key="$route.name" 只在路由名称变化时重建,参数变化不触发
我通常会根据场景选。纯后台菜单跳转用 $route.path 就够了;如果同一个页面依赖 query 展示不同内容,就得用 $route.fullPath。还有一种情况是路由配置了 name,且你希望参数变化不触发重挂载,那可以直接用 $route.name。
3.3 mode="out-in"还是默认同时过渡
默认情况下,Transition 的进入和离开是同时发生的:旧页面淡出、新页面淡入,两个页面在过渡期间会重叠在一起。对于元素级别的按钮、弹窗,这种同时过渡很自然。但对于整页切换,同时过渡会带来一个体验问题:两个页面并存时,滚动条和焦点会瞬间错乱,用户鼠标滚动的其实是新页面,但旧页面可能还占着原来的位置。
给 transition 加 mode="out-in" 会把顺序改成"先让旧页面完整退场,再把新页面放进场"。也就是说,离开动画执行完毕后,才挂载新组件、执行进入动画。这个模式对于路由页面切换几乎是标配,视觉上干净利落,也能避开双页面重叠的一系列布局问题。
mode="in-out" 则反过来:先进入新元素,等待进入完成后再执行旧元素的离开。这个模式用得很少,常见的场景是切换 tab 时想先展示新内容再移除旧内容,但一般会带来更多视觉混乱,除非你有明确的交互诉求,否则不推荐在页面切换中用它。
4. 按路由meta切换动画方向与名称:后台管理系统的高级玩法
4.1 在路由meta里直接声明动画名
实际的项目里,所有页面共用同一个淡入淡出动画往往不够。比如后台管理系统的菜单跳转通常希望是左右滑动,菜单A在左侧,切到右侧的菜单B时内容从右往左滑入;返回时则希望方向相反。最简单的方式,是在路由配置的 meta 里声明每个页面要用的动画名。
const routes = [ { path: '/dashboard', component: Dashboard, meta: { transition: 'page-fade' } }, { path: '/users', component: UserList, meta: { transition: 'slide-left' } } ]然后在布局组件里通过 useRoute 读取当前路由的 meta:
import { useRoute } from 'vue-router' import { computed } from 'vue' const route = useRoute() const transitionName = computed(() => route.meta.transition || 'page-fade')模板里绑定即可:
<transition :name="transitionName" mode="out-in"> <component :is="Component" :key="$route.path" /> </transition>大概意思就是"一个路由一个动画名"。好处是扩展性极强,新增页面时只要在路由配置里加一个 meta 字段,不需要改动布局组件。
4.2 通过路由深度判断进入/离开方向
meta 里写死动画名只解决了"每个页面用什么动画"的问题,还解决不了"前进和返回的方向感"。用户从列表页进入详情页,应该是内容左滑进入;从详情页返回列表页,应该右滑退出。方向是由路由跳转的前进/后退决定的,而不是由目标页面自身决定的。
一个常用做法是在路由 meta 里给页面设定深度 depth,然后在路由切换时比较 to 和 from 的深度:
let prevDepth = 0 watch(() => route.fullPath, () => { const currentDepth = route.meta.depth ?? 0 transitionName.value = currentDepth >= prevDepth ? 'page-next' : 'page-prev' prevDepth = currentDepth })如果目标页面的 depth 比当前大,说明是前进,用 page-next;反之说明是返回,用 page-prev。两个动画方向的 CSS 刚好是相反的。这个方案需要你维护好路由的 depth 层级,整体结构清晰时效果非常好。
如果你不想在建路由时频繁维护 depth,也可以直接基于菜单层级用程序计算深度,但那种方案侵入性更强,反而不好维护。我在实际项目里更推荐"meta 配 depth"这种显式声明,毕竟一个系统的层级结构通常不会频繁变动。
4.3 计算属性与watch route的取舍
上面的示例里用到了 watch。有的场景也可以直接用 computed 根据当前路由算动画名,但从"方向感"这个需求来看,计算属性天然拿不到上一次路由的深度,因为当前 route 对象只有一个,没有 before 和 after 的对比。所以涉及方向判断时更适合用 watch 配合一个外部变量,保存上一次深度。如果你只是想让不同页面有不同的静态动画,完全没有方向感需求,直接用 computed 读 route.meta 最省事。
还有一点需要注意:watch 里维护的 prevDepth 是在组件实例上的,如果整个布局组件被 keep-alive 缓存,或者组件卸载后重新挂载,prevDepth 可能会重置。多数后台管理系统的布局组件是常驻的,这个问题不常见,但如果你做的是多标签页系统,每个 Tab 页面各自带路由切换,就要小心 prevDepth 的初始化。
5. 进阶动画方案:命名过渡、CSS @keyframes与第三方动画库的取舍
5.1 只用transform和opacity的黄金法则
页面切换动画的性能,往往比拼的不是谁写得花哨,而是谁让浏览器的重排次数更少。浏览器每次渲染要经过"样式计算、布局、绘制、合成"这几个阶段,其中布局最贵。只要动画里改了 height、margin、top 这类几何属性,就可能触发整棵渲染树的重新布局,页面明显掉帧。而 transform(包含 translate、scale、rotate)和 opacity 的动画可以交给合成器线程处理,大部分情况下不触发重排重绘,性能开销小得多。
所以给页面切换做动画,我的建议永远是:尽量只用 transform 和 opacity。滑入滑出就用 translateX,淡入淡出就用 opacity,二者组合就已经能覆盖绝大多数页面切换需求。如果你在动画里看到了 height、width、font-size、top、left 这类属性,先停下来想想能不能换成 transform。确实,有些场景你想做一个高度从0被撑开的过渡,transform做不到,这种场景实现代价很高,通常我会直接放弃动画或者换一种交互方式,而不是硬撑着做性能杀手。
5.2 用@keyframes定义整套页面进入/离开动画
命名过渡配合 @keyframes 可以实现比单纯 transition 更丰富的效果。比如一个"淡入+轻微上移"的组合:
.page-enter-active { animation: page-in 0.4s ease-out; } .page-leave-active { animation: page-out 0.3s ease-in forwards; } @keyframes page-in { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } @keyframes page-out { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-16px); } }注意这里的动画名和过渡类名的关系。如果你设置了 name="page",那么 page-enter-active 和 page-leave-active 里的 animation 才会生效。一个容易忽略的点是:@keyframes 动画过程中,如果需要保持结束状态,得在动画里加上 forwards 或者用 transition 的方式设置目标值。否则动画结束后元素会回到默认样式,视觉上会产生回弹感。
为什么有的页面切出去时动画不完整?常见原因就是 leave-active 里的 animation 没有设置 forwards,动画播完后浏览器把元素恢复成初始样式,而这时DOM还没被移除,看起来像"闪了一下"。这个问题在排查时非常经典。
5.3 自定义类名接入animate.css这类CSS动画库
如果你不想手写每个动画,第三方的 animate.css 是一个很成熟的选择。Vue3 的 Transition 组件支持自定义过渡类名,可以完全覆盖默认的命名类:
<transition enter-active-class="animate__animated animate__fadeInRight" leave-active-class="animate__animated animate__fadeOutLeft" mode="out-in" > <component :is="Component" :key="$route.path" /> </transition>只要在项目里引入过 animate.css,对应动画类名就会生效。这里有一个坑:animate.css 的类名默认带 animate__ 前缀,enter-active-class 里既要写 animate__animated 又要写具体的动画类,否则不会动。另外第三方库的动画时长通常偏快,如果你觉得太快,可以再包一层自定义类覆盖 animation-duration。
不过让我说句实在话:animate.css 适合快速原型验证,或者项目里有大量通用动画需求时用。真正做长期维护的系统,我更倾向于自己维护几个固定的 keyframes。第三方库的类名是英文语义,业务同事很难一眼看懂页面上到底用了什么动画,而且随着版本升级,类名还可能变化,增加排查成本。我自己一般在项目里会维护一个 _transition.scss 文件,把所有页面动画统一收口,谁要改动画去这个文件里找,清晰得多。
6. 当CSS不够用时:用JS钩子完全接管页面过渡动画
6.1 JS钩子到底能接管什么
CSS 能覆盖绝大多数页面切换场景,但总有例外。比如你希望进入动画是"页面元素逐个飞入"这种依赖 JS 去频繁操作多个子元素的复杂效果,或者动画过程中需要中途终止、需要根据用户行为动态改变动画参数,这些东西纯 CSS 写会很吃力。Vue3 的 Transition 提供了八个JS钩子,可以让你在进入/离开的每个阶段拿到 DOM 和 done 回调,完全接管动画过程。
这八个钩子分别是 before-enter、enter、after-enter、enter-cancelled、before-leave、leave、after-leave、leave-cancelled。其中 enter 和 leave 钩子需要你主动调用第二个参数 done,告诉 Vue"我这个动画播完了",Vue 才会继续后面的流程。
6.2 before-enter到after-leave一套完整示例
下面是一个用 Web Animations API 实现页面滑入滑出的示例,不引入任何外部库:
<template> <router-view v-slot="{ Component }"> <transition :css="false" mode="out-in" @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter" @before-leave="beforeLeave" @leave="leave" @after-leave="afterLeave" > <component :is="Component" :key="$route.path" /> </transition> </router-view> </template> <script setup> function beforeEnter(el) { el.style.opacity = '0' el.style.transform = 'translateX(40px)' } function enter(el, done) { el.animate([ { opacity: 0, transform: 'translateX(40px)' }, { opacity: 1, transform: 'translateX(0)' } ], { duration: 350, easing: 'cubic-bezier(0.25, 0.1, 0.25, 1)' }).finished.then(done) } function afterEnter(el) { el.style.opacity = '1' el.style.transform = 'none' } function beforeLeave(el) { el.style.opacity = '1' } function leave(el, done) { el.animate([ { opacity: 1, transform: 'translateX(0)' }, { opacity: 0, transform: 'translateX(-40px)' } ], { duration: 300, easing: 'ease-in' }).finished.then(done) } function afterLeave(el) { el.style.opacity = '' el.style.transform = '' } </script>关键在于 :css="false"。这个属性是告诉 Vue"动画全权交给 JS,不要再用默认的类名去做样式处理",避免Vue在背后又去清除CSS类导致时序错乱。设置成 false 之后,初始样式和结束样式就得你自己处理,比如 before-enter 里先把透明度设为0,否则用户会看到元素先闪原样再飞入。
Web Animations API 是浏览器原生API,不需要额外安装什么库,兼容性在现代浏览器里已经足够。如果你想用 gsap、anime.js 这类库,原理完全一样,在钩子里调用库的动画方法,然后在回调里执行 done 就行。
6.3 忘记调用done和:css="false"这两个致命细节
JS钩子方案有两个非常经典的翻车点。第一个是忘了在 enter 或 leave 里调用 done,Vue 会一直认为动画还没结束,新旧页面都卡在"过渡中"的状态,页面就卡死了。用 Web Animations API 时记得用 .finished.then(done),用 requestAnimationFrame 写循环的话,也一定要在最后调用。
第二个是用了 JS 钩子但忘了加 :css="false"。这种情况下 Vue 还是会用CSS类名去处理元素,而你的JS也同时在操作样式,两边打架,动画状态会非常随机。建议要么纯CSS,要么 :css="false" 之后纯JS,不要混着来。如果你既想用JS钩子,又希望让CSS参与一部分过渡,那就要非常清楚每一步谁在改什么,否则后续排查成本极高。
7. 实测最容易翻车的五个细节与一套排查链路
7.1 初始加载页面没有动画:被忽略的appear
我见过不少人在路由过渡里遇到的第一问题是:"我明明加了transition,为什么首次进入页面时没有动画?"因为默认情况下 Transition 只对后续的切换生效,组件首次挂载不算"进入"。解决方式就是前面说的,给 Transition 加上 appear,初始挂载时也会走进入动画。注意,加了 appear 之后,如果页面是异步组件,首次挂载时动画和异步加载的时序也要观察一下,避免出现白屏后突然出现一个已经从中间状态开始的页面。
7.2 新旧页面同时闪现、滚动条乱跳:缺少mode
第二个高频问题是,切路由时新页面和旧页面会同时出现在屏幕上,甚至滚动条直接跳到顶部,体验很突兀。原因就是没有设置 mode="out-in"。默认的同时过渡在元素级别很自然,但整页切换时两个页面叠加,滚动容器和焦点全部错乱。我遇到不少后台系统的菜单跳转动画"抖得不行",排查下来基本都是这个原因。设置 mode="out-in" 后,旧页面走完离开动画,新页面才开始进入,两个页面不会重叠。
7.3 加了key路由跳转反而空白:组件多根节点问题
Transition 要求内部只有一个根节点,如果你路由页面组件写成了多根节点(比如并列渲染了多个顶层div),切换时 Vue 会提示 <Transition> expects exactly one child element or component,动画不生效,页面甚至可能空白。处理方式很简单:把页面组件最外层统一包一层div。这个规则对绝大多数后台系统是无感的,但如果你现在正在模板上用了 Fragment 或者多根节点的新写法,就要特别留意。
7.4 排查一套问题的方法论:从类名到CSS再到执行队列
遇到过渡动画不生效,我建议按一套固定的链路去排查,效率最高:
- 打开浏览器开发者工具的元素面板,切换路由,看目标元素上有没有出现 enter-from、enter-active、enter-to 这些类名。
- 如果类名出现了,说明 Transition 组件捕捉到了切换,问题大概率出在CSS上。检查一下类名前缀是否和 name 一致,transition 属性是否写在 active 类上,动画是否被其他全局样式覆盖。特别注意是否在 enter-to 里写了 transition,而 enter-active 里反而没有,这种颠倒会导致过渡不起作用。
- 如果类名没出现,说明组件压根没有走到过渡流程。优先检查触发条件:是不是 v-show 而不是 v-if?是不是组件被复用了所以没有重新挂载?是不是没加 key?
- 最后检查一下第三方库或全局CSS,有没有写 *{ transition: all } 这类的全局规则,很容易和其他动画打架。
这套链路我每次排查都先用,十次里能解决八次。真正涉及JS钩子的场景,再额外打开控制台看有没有 done 未调用导致的卡死,以及 :css="false" 有没有写对。
7.5 keep-alive与transition组合时的过渡损耗
最后提一下 keep-alive。后台系统的 Tab 页缓存经常会跟 transition 一起用,结构通常是:
<router-view v-slot="{ Component }"> <transition name="fade" mode="out-in"> <keep-alive> <component :is="Component" :key="$route.path" /> </keep-alive> </transition> </router-view>这个结构本身是合理的,但要注意:keep-alive 会缓存组件实例,组件首次创建时会走进入动画,之后再次进入时由于实例被缓存,过渡可能变成一种"快速闪现"。如果你的业务要求每个页面标签切换都要有完整的过渡,可能需要把 keep-alive 挪到 transition 外层,让 transition 感知的是缓存组件的重新挂载。这个顺序问题在不同项目里表现不一样,最好在集成时专门验证一下。
还有一种常见错误是 keep-alive 的 include 配置里没注册组件名,导致缓存根本没生效,页面每次切换都重新创建组件,过渡倒是正常了,但数据也每次重新拉取。这个坑也帮大家避一下。
8. 页面切换动画的体感调优:时长、缓动、减少动态与性能观察
8.1 不同设备与场景下的合理动画时长
页面切换动画时长有一个隐性平衡:太短等于没有,太长让人烦躁。我自己常用的范围是,PC 端整页切换 250~400ms,移动端 150~300ms。进入动画可以稍微比离开长一点,比如进入 350ms、离开 250ms,视觉重心会落在新页面上。
如果你做的后台系统里有大量列表页,动画时长更要克制。用户高频地在菜单间切换时,任何超过 400ms 的动画都会被当成"卡"。相反,如果是面向C端用户的H5活动页,带一点戏剧化的页面切换效果反而能提升氛围。
8.2 缓动曲线和减少动态效果
缓动曲线决定了动画在视觉上的"质感"。无脑用 ease 或 linear 会让页面切换看起来非常机械。我推荐进入动画用 ease-out(开始快,结束慢),让新页面迅速稳定下来;离开动画用 ease-in(开始慢,结束快),旧页面加速消失。如果你想要更精细的控制,可以写 cubic-bezier(0.25, 0.1, 0.25, 1) 这样的标准曲线。
另外还有一个容易被忽略的无障碍问题:部分用户会在系统里开启"减少动态效果"。CSS 层面可以通过 prefers-reduced-motion 检测,然后直接把过渡时长归零或缩短:
@media (prefers-reduced-motion: reduce) { .page-enter-active, .page-leave-active { transition: none; animation: none; } }这不只是加分项,很多系统在政务、医疗等场景,大量用户其实就是想要简洁的无动画体验。动画做得好很重要,但在用户明确表达"不要动画"时能体面地退场,同样重要。
8.3 大屏可视化项目中的性能观察
最后聊一个很多人忽略的场景:如果页面里嵌了 echarts、mapbox 这类重渲染库,页面切换动画期间,这些库的 canvas 或 WebGL 场景会跟随组件一起重绘。本身切换加的是 translateX 和 opacity,这两个属性理论上不触发重排,但 canvas 内容的重新绘制仍然不可避免。我实测过,大屏看板上如果某个页面有实时刷新的图表,切走时还在执行 leave 动画,图表会继续占用渲染资源,导致切页动画掉帧。
处理办法有两个思路:一是切换时在 beforeLeave 钩子里临时暂停图表刷新,切完了再恢复;二是在动画期间把页面渲染层做位图缓存,这个比较激进,一般用不到。最基本的原则是,页面切换动画不要做得太长,图表类页面优先保证帧率而不是追求酷炫效果。我自己的经验是,大屏项目里页面切换用 200ms 以内的纯淡入淡出,体验远比长距离滑动好。
关于Vue3的Transition组件,我能分享的实操经验大概就是这些。从类名的运转机制到路由插槽的正确组合,再到按meta动态切换动画、JS钩子接管复杂效果,最后是各种翻车现场的排查思路。做动画这件事,最忌讳的是为了动画而动画,页面切换动画的价值在于帮用户理解"我从哪来、要到哪去",所以克制、平滑、不打扰,才是真正舒服的过渡。如果你在项目里也遇到了 Transition 相关的奇怪问题,照着第7部分的排查链路走一遍,多数都能定位到根因。