观众朋友们,大家好! 今天咱们来聊聊 Vue 3 源码里那些“跳舞”的家伙——transition
和 transition-group
组件。别看它们名字普普通通,背后可是藏着不少秘密,尤其是它们和 CSS 动画类名之间的“爱恨情仇”。 准备好,咱们要开始“扒皮”了!
一、开场白:Vue 3 动画的魔法棒
在前端的世界里,动画就像魔法,能让用户界面瞬间活泼起来。 Vue 3 提供了 transition
和 transition-group
这两个强大的组件,让我们能轻松地为元素添加各种炫酷的动画效果。 它们就像两根魔法棒,挥一挥就能让元素飞起来,转个圈,或者淡入淡出。 但要真正玩转它们,就得了解它们的工作原理,特别是它们是如何与 CSS 动画类名配合的。
二、transition
组件:单元素动画的舞者
transition
组件主要用于单个元素或组件的动画。 它的核心思想是:当被包裹的元素插入、更新或移除 DOM 时,Vue 会自动添加或移除一些特定的 CSS 类名,然后我们就可以利用这些类名来定义动画效果。
- 基本用法:让元素“翩翩起舞”
最简单的用法是,把需要动画的元素用 transition
组件包裹起来。 例如:
<template>
<div>
<button @click="show = !show">Toggle</button>
<transition name="fade">
<p v-if="show">Hello, Vue 3!</p>
</transition>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const show = ref(false);
return { show };
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
在这个例子中,我们用 transition
组件包裹了一个 p
元素,并设置了 name
属性为 "fade"。 这意味着 Vue 会在元素进入和离开 DOM 时,自动添加以下 CSS 类名:
类名 | 描述 |
---|---|
.fade-enter-from |
在元素插入 DOM 前添加,表示动画的起始状态。 |
.fade-enter-active |
在元素插入 DOM 时添加,并在整个进入动画过程中保持存在。 用于定义动画的过渡效果(transition 属性)。 |
.fade-enter-to |
在元素插入 DOM 后(且 enter 钩子函数执行完毕后)添加。通常与 .fade-enter-from 配合使用,定义动画的结束状态。 Vue 会在下一个动画帧移除 .fade-enter-from ,触发过渡效果。 |
.fade-leave-from |
在元素移除 DOM 前添加,表示动画的起始状态。 |
.fade-leave-active |
在元素移除 DOM 时添加,并在整个离开动画过程中保持存在。 用于定义动画的过渡效果(transition 属性)。 |
.fade-leave-to |
在元素移除 DOM 后添加。通常与 .fade-leave-from 配合使用,定义动画的结束状态。 Vue 会在下一个动画帧移除 .fade-leave-from ,触发过渡效果。 |
我们只需要在 CSS 中定义这些类名的样式,就能实现动画效果。 在这个例子中,我们让元素淡入淡出。
- 源码剖析:Vue 是如何“施魔法”的?
那么,Vue 内部是如何实现这些类名的自动添加和移除的呢? 这就要深入到 Vue 3 的源码里看看了。
transition
组件的实现主要在 packages/runtime-core/src/components/Transition.ts
这个文件中。 简化后的核心逻辑如下:
// 简化后的 Transition 组件实现
export const Transition = {
name: 'Transition',
props: {
name: String,
// ... 其他 props
},
setup(props, { slots }) {
const enter = (el: Element, done: () => void) => {
// 添加 -enter-from 类名
addClass(el, `${props.name}-enter-from`);
nextFrame(() => {
// 移除 -enter-from 类名,添加 -enter-active 和 -enter-to 类名
removeClass(el, `${props.name}-enter-from`);
addClass(el, `${props.name}-enter-active`);
addClass(el, `${props.name}-enter-to`);
// 监听 transitionend 事件,动画结束后执行 done 回调
onTransitionEnd(el, () => {
removeClass(el, `${props.name}-enter-active`);
removeClass(el, `${props.name}-enter-to`);
done();
});
});
};
const leave = (el: Element, done: () => void) => {
// 添加 -leave-from 类名
addClass(el, `${props.name}-leave-from`);
nextFrame(() => {
// 移除 -leave-from 类名,添加 -leave-active 和 -leave-to 类名
removeClass(el, `${props.name}-leave-from`);
addClass(el, `${props.name}-leave-active`);
addClass(el, `${props.name}-leave-to`);
// 监听 transitionend 事件,动画结束后执行 done 回调
onTransitionEnd(el, () => {
removeClass(el, `${props.name}-leave-active`);
removeClass(el, `${props.name}-leave-to`);
done();
});
});
};
return () => {
const children = slots.default?.();
if (!children) {
return null;
}
const vnode = children[0];
if (!vnode) {
return null;
}
// 在 vnode 上添加 enter 和 leave 钩子函数
if (vnode.transition) {
vnode.transition.enter = enter;
vnode.transition.leave = leave;
} else {
vnode.transition = { enter, leave };
}
return vnode;
};
}
};
// 辅助函数:添加类名
function addClass(el: Element, className: string) {
el.classList.add(className);
}
// 辅助函数:移除类名
function removeClass(el: Element, className: string) {
el.classList.remove(className);
}
// 辅助函数:在下一个动画帧执行回调
function nextFrame(fn: () => void) {
requestAnimationFrame(() => {
requestAnimationFrame(fn);
});
}
// 辅助函数:监听 transitionend 事件
function onTransitionEnd(el: Element, cb: () => void) {
const remove = () => {
el.removeEventListener('transitionend', onEnd);
};
const onEnd = (e: TransitionEvent) => {
if (e.target === el) {
cb();
remove();
}
};
el.addEventListener('transitionend', onEnd);
}
这段代码的核心在于 enter
和 leave
函数。 当元素插入 DOM 时,enter
函数会被调用;当元素移除 DOM 时,leave
函数会被调用。 这些函数会按照一定的顺序添加和移除 CSS 类名,从而触发动画效果。
nextFrame
函数的作用是确保类名的添加和移除发生在不同的动画帧中,这样才能触发 CSS 的过渡效果。
onTransitionEnd
函数的作用是监听 transitionend
事件,当动画结束后,执行 done
回调函数,告诉 Vue 动画已经完成。
- 深入细节:
vnode.transition
是什么鬼?
在 transition
组件的 render
函数中,我们看到了 vnode.transition
这样的代码。 这玩意儿是干啥的?
实际上,vnode.transition
是 Vue 中用于处理过渡效果的一个内部机制。 当 transition
组件包裹一个元素时,它会在该元素的 VNode 上添加 transition
属性,并将 enter
和 leave
函数赋值给它。 这样,当 Vue 渲染该元素时,就会自动调用这些函数,从而触发动画效果。
- JavaScript 钩子函数:更高级的动画控制
除了 CSS 类名,transition
组件还提供了 JavaScript 钩子函数,让我们能更灵活地控制动画效果。 这些钩子函数包括:
钩子函数 | 描述 |
---|---|
beforeEnter |
在元素插入 DOM 之前调用。 |
enter |
在元素插入 DOM 时调用。 可以与 done 回调函数配合使用,手动控制动画的完成。 |
afterEnter |
在元素插入 DOM 之后调用。 |
enterCancelled |
当进入动画被取消时调用。 |
beforeLeave |
在元素移除 DOM 之前调用。 |
leave |
在元素移除 DOM 时调用。 可以与 done 回调函数配合使用,手动控制动画的完成。 |
afterLeave |
在元素移除 DOM 之后调用。 |
leaveCancelled |
当离开动画被取消时调用。 |
例如,我们可以使用 enter
钩子函数来实现一个更复杂的动画效果:
<template>
<div>
<button @click="show = !show">Toggle</button>
<transition
name="slide"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
>
<p v-if="show">Hello, Vue 3!</p>
</transition>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const show = ref(false);
const beforeEnter = (el) => {
el.style.transform = 'translateX(-100%)';
el.style.opacity = 0;
};
const enter = (el, done) => {
// 使用 JavaScript 实现动画
anime({
targets: el,
translateX: '0%',
opacity: 1,
duration: 500,
easing: 'easeOutElastic(1, .6)',
complete: done
});
};
const afterEnter = (el) => {
el.style.transform = ''; // 清除 style
el.style.opacity = ''; // 清除 style
};
return { show, beforeEnter, enter, afterEnter };
}
};
</script>
在这个例子中,我们使用了 Anime.js 库来实现一个滑动进入的动画效果。 在 enter
钩子函数中,我们调用 anime
函数来启动动画,并在动画完成后调用 done
回调函数。 使用 beforeEnter
来初始化 style,使用 afterEnter
来清除 style。
三、transition-group
组件:多元素动画的指挥家
transition-group
组件用于多个元素的动画。 它可以让我们轻松地为列表、网格等结构添加动画效果。
- 基本用法:让列表“动起来”
transition-group
组件的使用方式与 transition
组件类似,但有一些区别。 首先,transition-group
组件必须包裹在 v-for
指令生成的元素上。 其次,transition-group
组件需要指定一个 tag
属性,用于渲染包裹元素的标签。 例如:
<template>
<div>
<button @click="addItem">Add Item</button>
<transition-group name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</transition-group>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const items = ref([
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' }
]);
let nextId = 4;
const addItem = () => {
items.value.push({ id: nextId++, text: `Item ${nextId}` });
};
return { items, addItem };
}
};
</script>
<style>
.list-enter-active,
.list-leave-active {
transition: all 0.5s;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(30px);
}
.list-move {
transition: transform 0.5s;
}
</style>
在这个例子中,我们使用 transition-group
组件包裹了一个 ul
元素,并使用 v-for
指令生成了多个 li
元素。 当我们点击 "Add Item" 按钮时,会向 items
数组中添加一个新的元素,Vue 会自动为新的元素添加动画效果。
注意,除了 enter
和 leave
类名,transition-group
组件还提供了一个 move
类名。 这个类名用于定义元素在列表中的位置发生变化时的动画效果。
- 源码剖析:
transition-group
是如何处理多个元素的?
transition-group
组件的实现比 transition
组件要复杂一些,因为它需要处理多个元素的动画。 它的核心逻辑也在 packages/runtime-core/src/components/Transition.ts
这个文件中。 简化后的核心逻辑如下:
// 简化后的 TransitionGroup 组件实现
export const TransitionGroup = {
name: 'TransitionGroup',
props: {
tag: {
type: String,
default: 'span'
},
// ... 其他 props
},
setup(props, { slots }) {
return () => {
const children = slots.default?.();
if (!children) {
return null;
}
// 创建一个 Fragment VNode,用于包裹多个子元素
const fragment = createVNode(Fragment, null, children);
// 设置 Fragment VNode 的 key,避免被 Vue 优化掉
fragment.key = '__transition-group';
// 返回 Fragment VNode
return createVNode(props.tag, null, fragment);
};
}
};
transition-group
组件实际上并没有直接处理动画逻辑。 它的作用是创建一个 Fragment
VNode,用于包裹多个子元素。 然后,它会将 Fragment
VNode 渲染成一个指定的 HTML 标签(默认为 span
)。
动画逻辑实际上是由 Vue 的虚拟 DOM 更新机制来处理的。 当列表中的元素发生变化时,Vue 会比较新旧 VNode 树,找出需要添加、移除或移动的元素,并为这些元素添加相应的 CSS 类名,从而触发动画效果。
move
类名:让元素“滑动起来”
move
类名是 transition-group
组件特有的。 当列表中的元素位置发生变化时,Vue 会自动为该元素添加 move
类名。 我们可以利用这个类名来定义元素在列表中的滑动动画效果。
在上面的例子中,我们使用了以下 CSS 代码来定义 move
类名的样式:
.list-move {
transition: transform 0.5s;
}
这段代码表示,当元素的位置发生变化时,会使用 transform
属性来实现滑动动画,动画时长为 0.5 秒。
appear
属性:首次渲染时的动画
transition
和 transition-group
组件都提供了一个 appear
属性,用于控制首次渲染时的动画效果。 默认情况下,appear
属性的值为 false
,表示不启用首次渲染时的动画。 如果我们将 appear
属性设置为 true
,则 Vue 会在组件首次渲染时,自动添加 *-appear-from
、*-appear-active
和 *-appear-to
类名,从而触发动画效果。
例如:
<template>
<div>
<transition name="fade" appear>
<p v-if="show">Hello, Vue 3!</p>
</transition>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
const show = ref(false);
onMounted(() => {
show.value = true; // 首次渲染后显示元素
});
return { show };
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active,
.fade-appear-active {
transition: opacity 0.5s;
}
.fade-enter-from,
.fade-leave-to,
.fade-appear-from {
opacity: 0;
}
</style>
在这个例子中,我们将 transition
组件的 appear
属性设置为 true
。 这样,当组件首次渲染时,Vue 会自动添加 fade-appear-from
、fade-appear-active
和 fade-appear-to
类名,从而实现淡入动画效果。
四、总结:动画的艺术
transition
和 transition-group
组件是 Vue 3 中实现动画效果的利器。 它们通过自动添加和移除 CSS 类名,以及提供 JavaScript 钩子函数,让我们能轻松地为元素添加各种炫酷的动画效果。
要真正掌握这两个组件,需要深入理解它们的工作原理,特别是它们是如何与 CSS 动画类名配合的。 只有这样,才能将动画的艺术发挥到极致,为用户带来更好的体验。
组件 | 适用场景 | 核心机制 | 关键类名/属性 |
---|---|---|---|
transition |
单个元素或组件的动画 | 自动添加/移除 CSS 类名,触发过渡效果。 提供 JavaScript 钩子函数。 | enter-from , enter-active , enter-to , leave-* |
transition-group |
多个元素的动画(如列表、网格) | 创建 Fragment VNode,利用 Vue 的虚拟 DOM 更新机制处理动画。 | move , tag |
希望今天的讲解能帮助大家更好地理解 Vue 3 中 transition
和 transition-group
组件的实现原理。 动画的世界是无限的, 祝大家在动画的海洋里玩得开心!下次再见!