讲解

界面元素的出现和消失,如果瞬间切换会很生硬。Vue 的 <Transition> 组件把「进入」和「离开」变成有过程的动画:把需要过渡的元素包起来,Vue 会在合适的时机自动添加/移除一组 CSS class,你只需要写 CSS 过渡规则。核心 class 六个:v-enter-from / v-enter-active / v-enter-to(进入的起点、过程、终点)和 v-leave-from / v-leave-active / v-leave-to(离开三段),配合 transition 或 animation 属性就能工作。

实际使用记住三个要点。第一,用 name 属性定制 class 前缀:<Transition name="fade"> 对应 .fade-enter-from 等,避免多个过渡互相干扰。第二,离开过渡期间元素仍在 DOM 中(只是加了 class),所以 v-if 切换的元素可以放心包 Transition。第三,mode="out-in" 解决「新旧元素同时占位」的跳动问题——先让旧的离开完,再让新的进入。

列表的增删改用 <TransitionGroup>:它渲染真实容器元素(tag="ul"),对子项的进入/离开/移动都做过渡,移动动画基于 FLIP 技术自动计算位移。配合 v-for 使用,子项必须有稳定 key。

什么时候不用 Transition?需要 JS 驱动的高自由度动画(拖拽轨迹、弹簧物理、滚动驱动)用 requestAnimationFrame 或动画库(GSAP 等),Transition 管的是「状态切换时的入场/出场」这一类声明式动画。

示例

把过渡的 class 状态机用 JS 精确模拟:一个元素从「进入」到「离开」经过哪些状态(在本教程构建时被真实执行):

import assert from 'node:assert/strict';

// Transition 的状态机:记录进入/离开各阶段施加的 class
function transition(name) {
  const log = [];
  return {
    enter() {
      log.push(name + '-enter-from'); // 第 1 帧:起点
      log.push(name + '-enter-active'); // 第 1 帧:过程(含 transition 定义)
      // 下一帧:
      log.push('移除 ' + name + '-enter-from');
      log.push(name + '-enter-to'); // 终点
      // 过渡结束后:
      log.push('移除 ' + name + '-enter-active / ' + name + '-enter-to');
    },
    leave() {
      log.push(name + '-leave-from');
      log.push(name + '-leave-active');
      log.push('移除 ' + name + '-leave-from');
      log.push(name + '-leave-to');
      log.push('过渡结束,元素从 DOM 移除');
    },
    log,
  };
}

const t = transition('fade');
t.enter();
t.leave();

assert.deepStrictEqual(t.log, [
  'fade-enter-from',
  'fade-enter-active',
  '移除 fade-enter-from',
  'fade-enter-to',
  '移除 fade-enter-active / fade-enter-to',
  'fade-leave-from',
  'fade-leave-active',
  '移除 fade-leave-from',
  'fade-leave-to',
  '过渡结束,元素从 DOM 移除',
]);

// TransitionGroup 的移动检测(FLIP 思想:记录旧位置,比对的新位置)
const before = { a: 0, b: 60, c: 120 }; // 每项的 y 坐标
const after = { a: 60, c: 0, d: 120 }; // b 被删、c 上移、d 新增
const moved = Object.keys(after).filter((k) => before[k] !== undefined && before[k] !== after[k]);
const added = Object.keys(after).filter((k) => before[k] === undefined);
assert.deepStrictEqual(moved, ['a', 'c']);
assert.deepStrictEqual(added, ['d']);

console.log('过渡 class 序列共', t.log.length, '步,首尾两帧由浏览器自动过渡');
console.log('列表变化: 移动', moved.join('/'), ',新增', added.join('/'), ',删除 b');

模板与 CSS:

<script setup>
import { ref } from 'vue';

const show = ref(true);
const items = ref([1, 2, 3]);
let next = 4;
</script>

<template>
  <button @click="show = !show">切换</button>
  <Transition name="fade">
    <p v-if="show">我会淡入淡出</p>
  </Transition>

  <button @click="items.push(next++)">添加</button>
  <TransitionGroup tag="ul" name="list">
    <li v-for="i in items" :key="i" @click="items = items.filter((x) => x !== i)">
      第 {{ i }} 项(点击删除)
    </li>
  </TransitionGroup>
</template>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
.list-move,
.list-enter-active,
.list-leave-active {
  transition: all 0.3s;
}
.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}
</style>

常见坑

  • 只写 enter 忘了 leave:进入有动画离开秒消失,六个 class 成对理解。
  • Transition 包了多个根元素:它只接管单根节点,多根要用 v-if/v-else 保证同时只有一个。
  • TransitionGroup 的子项没 key:移动动画依赖 key 识别元素,缺 key 全部按进入/离开处理。
  • transition 属性写错 class:持续时间要写在 *-active 上(或 from/to 上用 animation),写在 to 上会出现「结束时才过渡」。
  • CSS 没有初始帧概念:enter-from 只存在一帧就被移除,别指望它长期存在;调试时给 active 加很长 duration 观察。

小结

Transition 自动施加进入/离开三段 class,你写 CSS;TransitionGroup 管列表增删移动;复杂物理动画交给 JS 动画库。下一章扩展模板能力:自定义指令。