Vue 基础体系 · 第 32/70 篇。示例基于 Vue 3、Composition API、TypeScript 与现代 Vite 工具链;版本敏感能力会单独标注。
Vue Transition 与 TransitionGroup:进入离开、列表动画和性能
在 Vue 3 中,<Transition> 和 <TransitionGroup> 都用于把“状态变化”转换为“DOM 变化过程中的视觉效果”。
两者解决的问题不同:
Transition:处理一个元素、一个组件,或一个动态组件的进入与离开。TransitionGroup:处理一组带有唯一key的列表项,包括列表项的进入、离开和位置移动。
它们并不负责定义动画本身。Vue 负责在正确的生命周期阶段添加、移除 CSS 类,或调用 JavaScript 钩子;真正的动画通常由 CSS transition、CSS animation 或 JavaScript 动画库完成。
一、进入和离开动画的前提:状态变化必须产生 DOM 变化
动画的触发源不是“点击按钮”,而是响应式状态变化导致的条件渲染变化。
例如:
<script setup lang="ts">
import { ref } from 'vue'
const visible = ref(false)
</script>
<template>
<button @click="visible = !visible">
{{ visible ? '隐藏' : '显示' }}
</button>
<p v-if="visible">这段内容会进入或离开。</p>
</template>
当 visible 从 false 变为 true 时,p 元素被创建;从 true 变为 false 时,元素被移除。Vue 的 <Transition> 正是介入这两个过程:
<Transition name="fade">
<p v-if="visible">这段内容会进入或离开。</p>
</Transition>
这里的 name="fade" 会决定 Vue 使用哪些 CSS 类名。它不会自动产生淡入淡出效果,仍然需要定义 CSS。
二、Transition 的进入与离开阶段
对于 name="fade" 的 <Transition>,Vue 3 会使用以下类:
| 阶段 | CSS 类 | 作用 |
|---|---|---|
| 进入初始状态 | .fade-enter-from |
元素刚插入时的状态 |
| 进入活动状态 | .fade-enter-active |
进入过程持续期间存在 |
| 进入结束状态 | .fade-enter-to |
元素进入完成时的状态 |
| 离开初始状态 | .fade-leave-from |
元素开始离开时的状态 |
| 离开活动状态 | .fade-leave-active |
离开过程持续期间存在 |
| 离开结束状态 | .fade-leave-to |
元素即将被移除时的状态 |
一个完整的淡入淡出示例:
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition:
opacity 180ms ease,
transform 180ms ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(8px);
}
.fade-enter-to,
.fade-leave-from {
opacity: 1;
transform: translateY(0);
}
</style>
进入过程可以理解为:
插入元素
↓
fade-enter-from + fade-enter-active
↓ 下一帧
移除 fade-enter-from,加入 fade-enter-to
↓ transition 完成
移除全部 enter 类
离开过程则是:
开始离开
↓
fade-leave-from + fade-leave-active
↓ 下一帧
移除 fade-leave-from,加入 fade-leave-to
↓ transition 完成
移除元素
transition 属性通常写在 *-enter-active 和 *-leave-active 上,因为这两个类覆盖整个动画期间。
进入和离开的同时性
默认情况下,旧节点离开和新节点进入可能同时发生。例如:
<Transition name="fade">
<p v-if="visible" key="shown">显示状态</p>
<p v-else key="hidden">隐藏状态</p>
</Transition>
同一时刻只能有一个子节点,但两个节点切换时,默认模式允许离开和进入重叠。对于简单淡入淡出,这通常是合理的;对于页面或面板切换,重叠可能导致两个内容同时占据布局空间。
三、mode:控制旧节点和新节点的先后关系
<Transition> 支持三种常用模式。
1. 默认模式:同时进入和离开
<Transition name="fade">
<component :is="currentComponent" />
</Transition>
旧组件开始离开的同时,新组件开始进入。
2. out-in:先离开,再进入
<Transition name="fade" mode="out-in">
<component :is="currentComponent" />
</Transition>
流程是:
旧组件 leave
↓ 完成
旧组件从 DOM 移除
↓
新组件 enter
适合内容区域切换,避免新旧内容重叠。代价是切换期间会出现一个短暂的空档。
3. in-out:先进入,再离开
<Transition name="fade" mode="in-out">
<component :is="currentComponent" />
</Transition>
流程相反:
新组件 enter
↓ 完成
旧组件 leave
适合希望内容连续存在、不出现空白的场景,但新旧内容可能暂时同时占据空间。
mode 只适用于 <Transition>,不适用于 <TransitionGroup>。列表中有多个子节点,每个子节点的进入、离开和移动需要独立协调,因此不存在“整个列表先离开再进入”的单一模式。
四、完整的 Vue 3 + TypeScript 示例
下面的组件同时演示:
- 单个元素的进入和离开;
mode="out-in"的内容切换;- 列表项进入和离开;
- 列表顺序变化时的位置动画;
v-show场景;- 稳定的列表
key。
<script setup lang="ts">
import { computed, ref } from 'vue'
type Item = {
id: number
label: string
}
const visible = ref(true)
const activePanel = ref<'a' | 'b'>('a')
const nextId = ref(4)
const items = ref<Item[]>([
{ id: 1, label: '编译' },
{ id: 2, label: '测试' },
{ id: 3, label: '部署' },
])
const reversed = ref(false)
const displayedItems = computed(() => {
return reversed.value ? [...items.value].reverse() : items.value
})
function addItem() {
const id = nextId.value++
items.value.push({
id,
label: `新任务 ${id}`,
})
}
function removeItem(id: number) {
items.value = items.value.filter(item => item.id !== id)
}
function reverseItems() {
reversed.value = !reversed.value
}
</script>
<template>
<main class="demo">
<section>
<h2>单元素进入与离开</h2>
<button @click="visible = !visible">
{{ visible ? '隐藏提示' : '显示提示' }}
</button>
<Transition name="fade">
<p v-if="visible" class="notice">
操作已完成
</p>
</Transition>
</section>
<section>
<h2>组件内容切换</h2>
<button @click="activePanel = activePanel === 'a' ? 'b' : 'a'">
切换面板
</button>
<Transition name="panel" mode="out-in">
<article v-if="activePanel === 'a'" key="panel-a" class="panel">
面板 A
</article>
<article v-else key="panel-b" class="panel">
面板 B
</article>
</Transition>
</section>
<section>
<h2>列表进入、离开和移动</h2>
<div class="actions">
<button @click="addItem">添加</button>
<button @click="reverseItems">反转顺序</button>
</div>
<TransitionGroup
name="rows"
tag="ul"
class="rows"
>
<li
v-for="item in displayedItems"
:key="item.id"
class="row"
>
<span>{{ item.label }}</span>
<button @click="removeItem(item.id)">删除</button>
</li>
</TransitionGroup>
</section>
<section>
<h2>v-show</h2>
<button @click="visible = !visible">
切换 display
</button>
<Transition name="fade">
<p v-show="visible" class="notice">
v-show 不销毁节点,只切换显示状态
</p>
</Transition>
</section>
</main>
</template>
<style scoped>
.demo {
max-width: 640px;
margin: 40px auto;
font-family: sans-serif;
}
section {
margin-block: 32px;
}
button {
cursor: pointer;
}
.notice,
.panel {
margin-top: 16px;
padding: 12px 16px;
border: 1px solid #cbd5e1;
border-radius: 8px;
}
.fade-enter-active,
.fade-leave-active {
transition:
opacity 180ms ease,
transform 180ms ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(8px);
}
.panel-enter-active,
.panel-leave-active {
transition:
opacity 220ms ease,
transform 220ms ease;
}
.panel-enter-from {
opacity: 0;
transform: translateX(16px);
}
.panel-leave-to {
opacity: 0;
transform: translateX(-16px);
}
.rows {
position: relative;
padding: 0;
list-style: none;
}
.row {
display: flex;
justify-content: space-between;
align-items: center;
margin-block: 8px;
padding: 10px 12px;
border: 1px solid #cbd5e1;
border-radius: 8px;
background: white;
}
/* 进入和离开 */
.rows-enter-active,
.rows-leave-active {
transition:
opacity 200ms ease,
transform 200ms ease;
}
.rows-enter-from,
.rows-leave-to {
opacity: 0;
transform: translateX(20px);
}
/*
* 离开的元素脱离普通流,其他元素才能立即移动到目标位置。
* 对垂直列表尤其常见。
*/
.rows-leave-active {
position: absolute;
width: 100%;
}
/* 列表位置移动 */
.rows-move {
transition: transform 250ms ease;
}
</style>
运行前提是已有一个 Vue 3 + Vite + TypeScript 项目,例如使用官方脚手架创建的项目。将代码放入 src/App.vue 后启动开发服务器,预期结果是:
- “操作已完成”在创建和销毁时淡入淡出;
- 面板切换时旧面板完成离开后新面板才进入;
- 添加和删除列表项有进入、离开动画;
- 反转列表时,未被删除的项目会平滑移动到新位置;
v-show对应的节点仍然存在,只改变其显示状态。
五、v-if 和 v-show 的动画差异
v-if:创建和销毁节点
<Transition name="fade">
<div v-if="visible">内容</div>
</Transition>
v-if 为真时创建节点,为假时执行离开过程,离开动画结束后从 DOM 中移除。
因此它适合:
- 内容确实不需要长期存在;
- 组件创建和销毁成本可以接受;
- 希望离开后释放 DOM 和组件资源。
v-show:保留节点,只切换显示状态
<Transition name="fade">
<div v-show="visible">内容</div>
</Transition>
v-show 通常通过修改 display 控制显示状态。Vue 仍然会调用进入和离开过渡,但节点不会被销毁。
因此它适合:
- 频繁显示和隐藏;
- 组件初始化成本较高;
- 需要保留输入内容、内部状态或 DOM 状态。
需要注意,v-show 的“隐藏”最终仍然涉及 display: none。如果内容需要在隐藏期间继续参与布局,v-show 并不能满足要求,应使用其他样式状态控制。
六、为什么列表必须使用稳定且唯一的 key
TransitionGroup 的直接子节点必须能够被 Vue 稳定识别。最重要的条件是:每个列表项都应有在其生命周期内保持稳定的唯一 key。
正确做法:
<TransitionGroup name="rows" tag="ul">
<li
v-for="item in items"
:key="item.id"
>
{{ item.label }}
</li>
</TransitionGroup>
常见错误是使用数组索引:
<li
v-for="(item, index) in items"
:key="index"
>
{{ item.label }}
</li>
考虑初始列表:
索引 0 → A
索引 1 → B
索引 2 → C
删除 A 后:
索引 0 → B
索引 1 → C
如果使用索引作为 key,Vue 会认为:
旧 key 0 的节点仍然是新 key 0
旧 key 1 的节点仍然是新 key 1
于是它倾向于复用节点并更新文本,而不是识别出“A 离开、B 和 C 向前移动”。这会导致:
- 离开动画作用到错误的内容;
- 输入框内部值和列表项错位;
- 组件本地状态被错误复用;
- 移动动画不符合用户看到的数据变化。
key 的意义不是“给循环变量编号”,而是声明“这个 DOM 节点代表哪个业务实体”。因此,数据库主键、客户端生成的唯一 ID 通常比索引更合适。
七、TransitionGroup 的三类动画
一个列表变化可能同时包含三种不同的变化。
1. 进入动画
新列表项出现时使用:
.rows-enter-from
.rows-enter-active
.rows-enter-to
2. 离开动画
列表项从数据中移除时使用:
.rows-leave-from
.rows-leave-active
.rows-leave-to
3. 移动动画
已有列表项的位置发生变化时使用:
.rows-move
移动动画并不是通过给每个元素设置 top 或 left 完成的。Vue 会在列表更新前后记录元素位置,然后通过变换把元素从旧位置“反向抵消”,再将 transform 过渡到目标位置。这类技术通常称为 FLIP:
- First:记录更新前的位置;
- Last:记录更新后的目标位置;
- Invert:用反向
transform把元素视觉上放回旧位置; - Play:过渡到
transform: none。
设某元素更新前的垂直位置是 ,更新后的目标位置是 ,则需要补偿的位移可以表示为:
如果元素实际向下移动了 40 像素,Vue 可以先让它保持 translateY(-40px) 的视觉位置,再把这个变换过渡到 translateY(0)。用户看到的就是元素从旧位置平滑移动到新位置。
这解释了两个重要边界:
key必须正确,否则 Vue 无法判断哪个节点是“同一个元素”;- 移动动画依赖真实布局位置,复杂嵌套布局、动态高度和绝对定位可能需要额外 CSS 或 JavaScript 协调。
八、为什么离开项经常需要 position: absolute
假设垂直列表中删除中间项:
A
B ← 删除
C
如果 B 执行离开动画期间仍处于普通文档流中,C 在动画结束前通常不会立即移动到 B 的位置。这样用户看到的是:
B淡出;B完全消失;C突然跳上来。
将离开项设置为绝对定位后:
.rows-leave-active {
position: absolute;
}
B 在离开阶段脱离普通流,C 可以立即占据目标位置;随后 C 通过 .rows-move 平滑移动。
这不是所有布局都必须使用的固定规则:
- 如果离开动画本身不要求其他元素同时移动,可以不设置;
- 如果列表项高度不固定,绝对定位后通常还需要处理宽度和高度;
- 网格、表格和复杂 flex 布局不能直接照搬垂直列表的 CSS。
九、TransitionGroup 的结构约束
TransitionGroup 与普通容器有几个不同点。
1. 默认不会强制生成包装元素
Vue 3 中,TransitionGroup 默认可以渲染为 fragment。如果需要明确的 HTML 容器,可以使用 tag:
<TransitionGroup name="rows" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.label }}
</li>
</TransitionGroup>
这里生成的容器是 ul,直接子节点是 li。
2. 直接子节点需要稳定 key
key 应写在 v-for 产生的直接子节点上,而不是只写在内部元素上:
<!-- 推荐 -->
<TransitionGroup name="rows">
<div
v-for="item in items"
:key="item.id"
>
{{ item.label }}
</div>
</TransitionGroup>
3. 列表项必须代表可区分的实体
如果两个项目的 key 重复,Vue 无法建立稳定的节点身份,可能出现运行时警告、状态错位或动画异常。唯一性不仅应在当前数组中成立,还应在相邻更新之间保持稳定。
十、JavaScript 钩子:动画由代码控制时必须完成回调
CSS 适合普通的透明度、位移和缩放效果。如果动画依赖测量高度、第三方动画库或复杂的时间线,可以使用 JavaScript 钩子。
<template>
<Transition
:css="false"
@enter="enter"
@leave="leave"
>
<div v-if="visible" class="box">
内容
</div>
</Transition>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const visible = ref(true)
function enter(
element: Element,
done: () => void
) {
const el = element as HTMLElement
el.animate(
[
{ opacity: 0, transform: 'scale(0.95)' },
{ opacity: 1, transform: 'scale(1)' },
],
{
duration: 220,
easing: 'ease-out',
fill: 'forwards',
},
).finished
.then(() => done())
.catch(() => done())
}
function leave(
element: Element,
done: () => void
) {
const el = element as HTMLElement
el.animate(
[
{ opacity: 1, transform: 'scale(1)' },
{ opacity: 0, transform: 'scale(0.95)' },
],
{
duration: 220,
easing: 'ease-in',
fill: 'forwards',
},
).finished
.then(() => done())
.catch(() => done())
}
</script>
这里的关键点是:
:css="false"告诉 Vue 不要等待 CSS 过渡;- 钩子接收
done时,必须在动画完成或失败后调用它; - 如果不调用
done,Vue 可能一直认为节点处于过渡中,离开节点无法被正确移除; - 使用
finished的 Promise 时,应处理失败路径,例如动画被取消或浏览器拒绝执行。
如果 JavaScript 钩子不接收 done,Vue 会把它视为同步钩子:
function beforeEnter(element: Element) {
// 只执行同步准备工作
}
需要区分“准备阶段”和“完成阶段”。同步钩子适合设置初始样式,异步动画则应明确调用 done。
CSS 和 JavaScript 混用
也可以保留 CSS 动画,同时使用钩子进行测量:
<Transition
name="fade"
@before-enter="prepare"
@after-enter="finish"
>
<div v-if="visible">内容</div>
</Transition>
如果钩子没有 done 参数,Vue 仍根据 CSS 过渡或动画的结束时间判断完成。CSS 声明的持续时间应与实际视觉效果一致,否则可能出现:
- DOM 已被移除但视觉动画尚未结束;
- 动画结束后节点仍短暂保留;
- 多个 CSS 属性中某个属性结束更晚,导致判断不符合预期。
对于嵌套动画或 CSS 时间难以自动推断的情况,可以显式设置持续时间:
<Transition
name="fade"
:duration="{ enter: 300, leave: 500 }"
>
<div v-if="visible">内容</div>
</Transition>
这里的数字单位是毫秒。它只控制 Vue 等待的时间,不会自动改变 CSS 动画的实际持续时间,因此两者应保持一致。
十一、生命周期钩子和取消路径
Transition 提供进入和离开的阶段钩子,例如:
<Transition
name="fade"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
>
<div v-if="visible">内容</div>
</Transition>
对应的状态变化可以表示为:
stateDiagram-v2
[*] --> Hidden
Hidden --> Entering: visible = true
Entering --> Visible: enter 完成
Entering --> Hidden: 进入被取消
Visible --> Leaving: visible = false
Leaving --> Hidden: leave 完成
Leaving --> Visible: 离开被取消
“取消”不是普通的“动画完成”。例如:
- 元素开始进入;
- 用户很快再次触发状态切换;
- 元素还没进入完成,就开始离开。
此时应清理进入阶段设置的临时样式或动画实例。否则可能出现:
- 元素残留
opacity: 0; - Web Animations API 的旧动画继续覆盖新状态;
- 多次触发后出现状态竞争。
如果使用第三方动画库,应在 enter-cancelled 和 leave-cancelled 中停止旧动画,并确保新的钩子可以重新接管元素。
十二、动态组件与 key:决定是更新还是重新过渡
动态组件切换通常这样写:
<Transition name="fade" mode="out-in">
<component
:is="currentComponent"
:key="currentComponent"
/>
</Transition>
key 的作用是明确告诉 Vue:组件类型变化时,这不是同一个节点的普通更新,而是一个节点离开、另一个节点进入。
如果没有合适的身份差异,Vue 可能复用同一个节点,只更新其内容。复用本身并不错误,但它不会必然触发你期待的进入和离开过渡。
同理,想让同一组件在参数变化时重新执行进入动画,也可以使用代表版本或实体身份的 key:
<Transition name="fade" mode="out-in">
<UserCard
:key="userId"
:user-id="userId"
/>
</Transition>
但不应无条件在每次渲染中生成随机 key。随机 key 会让 Vue 每次都销毁并重建节点,导致:
- 本地状态丢失;
- 输入焦点丢失;
- 组件生命周期被频繁执行;
- 动画和性能都变差。
十三、性能:动画属性、布局和列表规模
1. 优先动画化 transform 和 opacity
下面这些属性通常更适合动画:
transition:
opacity 180ms ease,
transform 180ms ease;
原因是它们通常不需要重新计算其他元素的布局。相比之下,动画化以下属性可能触发布局或绘制成本:
height
width
top
left
margin
padding
这不是说这些属性绝对不能动画,而是它们更容易影响周围元素的位置,需要浏览器重新计算布局。
例如,列表高度随展开变化时:
.panel {
transition: height 200ms ease;
}
如果高度从 auto 变为具体值,CSS 无法直接对 auto 做普通数值插值。此时通常需要:
- 使用固定高度或
max-height; - 使用 JavaScript 测量真实高度;
- 使用 Web Animations API 或动画库控制数值。
2. TransitionGroup 不会减少 DOM 数量
TransitionGroup 只是协调列表节点的过渡,不是虚拟列表组件。对于大量数据:
- 所有列表项仍然可能存在于 DOM;
- 离开项在动画完成前仍然保留;
- 位置移动需要读取和比较布局;
- 复杂列表可能造成布局计算和绘制压力。
如果列表规模很大,应首先解决渲染规模问题,例如分页、窗口化或虚拟滚动,然后再对可见窗口中的项目使用有限的动画。不能期待给几千个 DOM 节点包上 TransitionGroup 就获得高性能。
3. 不要在移动动画中强制布局
FLIP 需要读取元素位置,但如果在读取布局后立刻大量修改会影响布局的样式,可能产生强制同步布局。自定义 JS 列表动画时,应避免反复执行:
element.getBoundingClientRect()
element.style.height = ...
element.offsetHeight
element.getBoundingClientRect()
更合理的做法是批量读取位置,再批量写入样式,并让浏览器在合适的帧中执行更新。Vue 内部的列表移动协调属于框架实现,不应依赖其未公开的内部细节;自定义动画代码应把测量和写入阶段明确分开。
十四、减少动态效果:尊重用户的系统设置
部分用户会在操作系统中启用“减少动态效果”。可以通过媒体查询降低或关闭动画:
@media (prefers-reduced-motion: reduce) {
.fade-enter-active,
.fade-leave-active,
.panel-enter-active,
.panel-leave-active,
.rows-enter-active,
.rows-leave-active,
.rows-move {
transition-duration: 1ms;
}
}
这不会改变 Vue 的进入和离开生命周期,只是让视觉动画几乎立即完成。这样组件状态、DOM 生命周期和无障碍偏好之间仍然保持一致。
如果动画承担了重要信息,例如“保存中”到“保存成功”的状态提示,则不能只依赖颜色或位移动画传达语义,还应提供文本、ARIA 状态或其他可感知信息。
十五、常见失败表现与诊断方法
失败一:只有闪烁,没有动画
检查以下条件:
- 是否确实使用了
<Transition>或<TransitionGroup>; name与 CSS 类前缀是否一致;v-if或列表数据是否真的发生变化;transition是否写在*-enter-active、*-leave-active或*-move上;- CSS 是否被其他规则覆盖;
- 持续时间是否为
0。
例如:
<Transition name="fade">
必须对应:
.fade-enter-active { ... }
.fade-leave-active { ... }
写成 .transition-enter-active 不会匹配。
失败二:列表删除了错误的项目
优先检查 key。如果使用了数组索引,删除头部或中间项时最容易暴露问题。改为稳定的业务 ID:
:key="item.id"
同时确认 item.id 在列表中唯一,且不会因为排序或过滤而重新生成。
失败三:位置移动没有动画
检查:
.rows-move {
transition: transform 250ms ease;
}
还应确认:
- 列表顺序确实改变,而不是所有节点都被重新创建;
- 子节点有稳定 key;
- 元素的最终位置确实发生了变化;
- 没有其他 CSS 覆盖
transform; - 自定义样式没有让所有元素脱离正常布局。
失败四:离开钩子后节点一直不消失
通常是 JavaScript 钩子接收了 done,却没有调用:
function leave(element: Element, done: () => void) {
// 必须在动画完成或失败后调用 done()
}
如果使用 Promise,应同时处理 resolve 和 reject,避免动画被取消后永远不结束。
失败五:mode="out-in" 切换时出现空白时间过长
检查 CSS 实际持续时间和 duration 是否一致。若 CSS 设置了 500ms,但 duration 设置为 1000,Vue 会额外等待;反过来则可能提前结束生命周期。
失败六:动画期间页面高度跳动
这通常不是 Vue 错误,而是布局策略的结果。常见原因是:
- 离开节点仍在普通文档流中;
- 进入和离开节点同时存在;
- 列表项高度不一致;
- 离开项设置了绝对定位但没有保留宽度;
- 动画化了会影响布局的属性。
应根据目标效果选择 mode、position: absolute、固定宽度或其他布局方案,而不是只调整动画时长。
十六、规范保证、实现细节和工程取舍
可以明确区分三类结论。
Vue API 的规范能力
Vue 3 官方 API 保证了:
<Transition>用于单个元素或组件的进入、离开过渡;<TransitionGroup>用于列表项的进入、离开和移动过渡;- 过渡类名、钩子、
mode、appear、duration等属于公开能力; - 列表项需要稳定的
key才能正确识别节点身份。
常见实现机制
Vue 通过以下方式完成协调:
- 在进入和离开的不同阶段切换 CSS 类;
- 根据 CSS 过渡或动画结束时间判断完成;
- 对列表项更新前后的位置进行测量,以支持移动过渡;
- 在离开过渡结束前暂时保留离开节点。
具体的内部调度细节、DOM 测量时机和补丁算法属于实现细节,不应作为业务代码的依赖。
工程取舍
最终选择取决于交互目标:
- 简单显示隐藏:
Transition + opacity/transform; - 内容面板互斥切换:
mode="out-in"; - 高频切换但需要保留状态:
v-show; - 可排序、可插入、可删除的列表:
TransitionGroup + 稳定 key; - 大规模数据:先控制 DOM 规模,再考虑动画;
- 复杂时间线:JavaScript 钩子或专用动画库;
- 用户启用减少动态效果:通过
prefers-reduced-motion降低动画。
Transition 和 TransitionGroup 的核心不是“给页面加特效”,而是把响应式状态变化、节点身份、DOM 生命周期和视觉过渡连接起来。只要能准确回答“哪个节点被创建”“哪个节点被移除”“哪个节点只是移动”“Vue 何时认为过渡完成”,大多数进入、离开和列表动画问题都可以从机制层面定位。
系列导航与关联阅读
- 系列入口:Vue 完整学习路线:从响应式与组件到工程化、SSR 和生产交付
- 上一篇:Vue Plugin 与 Provide/Inject:依赖注入、类型和作用域
- 下一篇:Vue Suspense 与异步组件:加载、错误、超时和嵌套边界
官方资料
本文依据 Vue、Vite 与生态项目官方文档重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论