各位靓仔靓女,晚上好!我是你们的老朋友,今晚咱们聊点Vue 3里面比较有意思的东西: <Transition>
和 <TransitionGroup>
的动画钩子和类名切换逻辑。 别怕,听起来复杂,其实理清楚了也就那么回事儿。
开场白:动画这玩意儿,谁不喜欢?
话说回来,Web应用要是没有点动画效果,那简直就像喝白开水一样寡淡无味。 Vue 3 提供了强大的 <Transition>
和 <TransitionGroup>
组件,让我们能轻松地给组件添加各种各样的动画效果。 但是,这两个组件背后的动画钩子和类名切换逻辑,很多人可能感觉有点迷糊。 今天,我就来给大家扒一扒它们的底裤,让大家彻底搞明白!
第一部分:<Transition>
组件:单兵作战,优雅入场
<Transition>
组件主要用于单个元素或组件的过渡效果。 想象一下,一个按钮从屏幕外飞入,或者一个提示框缓缓淡出,这些都可以用 <Transition>
来实现。
1.1 基本用法:给你的元素穿上“动感战衣”
最简单的用法,就是用 <Transition>
把你想加动画的元素包起来:
<template>
<div>
<button @click="show = !show">Toggle</button>
<Transition name="fade">
<p v-if="show">Hello, world!</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 ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
这里,name="fade"
告诉Vue,我们要使用 fade-enter-from
、fade-enter-active
、fade-enter-to
、fade-leave-from
、fade-leave-active
、fade-leave-to
这些CSS类名来控制动画。
1.2 类名切换逻辑:动画的“三板斧”
<Transition>
组件在过渡过程中,会按照一定的顺序添加和移除CSS类名。 这就是动画的“三板斧”,掌握了这三板斧,就能玩转各种动画效果。
类名 | 作用 | 添加时机 | 移除时机 |
---|---|---|---|
v-enter-from |
定义进入过渡的开始状态。 | 元素插入DOM之前 | 进入过渡/动画的下一帧(保证浏览器渲染) |
v-enter-active |
定义进入过渡的生效状态。通常在这里设置transition 属性。 |
元素插入DOM之后 | 进入过渡/动画完成时 |
v-enter-to |
定义进入过渡的结束状态。 | 进入过渡/动画的下一帧(保证浏览器渲染) | 进入过渡/动画完成时 |
v-leave-from |
定义离开过渡的开始状态。 | 离开过渡开始时 | 离开过渡/动画的下一帧(保证浏览器渲染) |
v-leave-active |
定义离开过渡的生效状态。通常在这里设置transition 属性。 |
离开过渡开始时 | 离开过渡/动画完成时 |
v-leave-to |
定义离开过渡的结束状态。 | 离开过渡/动画的下一帧(保证浏览器渲染) | 元素从DOM移除时 |
v-
前缀: 这里的v-
可以替换成你通过name
属性指定的名称,比如fade-
。
1.3 JavaScript 钩子:更精细的控制
除了CSS类名,<Transition>
还提供了JavaScript钩子,让你可以在动画的各个阶段执行自定义的JavaScript代码。
<template>
<div>
<button @click="show = !show">Toggle</button>
<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"
>
<p v-if="show">Hello, world!</p>
</Transition>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const show = ref(false);
const beforeEnter = (el) => {
console.log('beforeEnter', el);
};
const enter = (el, done) => {
console.log('enter', el);
// 强制浏览器重绘,以应用初始样式
void el.offsetWidth;
el.classList.add('fade-enter-active');
el.addEventListener('transitionend', done);
};
const afterEnter = (el) => {
console.log('afterEnter', el);
el.classList.remove('fade-enter-active');
};
const enterCancelled = (el) => {
console.log('enterCancelled', el);
el.classList.remove('fade-enter-active');
};
const beforeLeave = (el) => {
console.log('beforeLeave', el);
};
const leave = (el, done) => {
console.log('leave', el);
void el.offsetWidth;
el.classList.add('fade-leave-active');
el.addEventListener('transitionend', done);
};
const afterLeave = (el) => {
console.log('afterLeave', el);
el.classList.remove('fade-leave-active');
};
const leaveCancelled = (el) => {
console.log('leaveCancelled', el);
el.classList.remove('fade-leave-active');
};
return { show, beforeEnter, enter, afterEnter, enterCancelled, beforeLeave, leave, afterLeave, leaveCancelled };
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
el
: 指的是被过渡的元素。done
: 是一个回调函数,必须在enter
和leave
钩子中调用,用来通知<Transition>
组件过渡已经完成。 如果你使用 CSS 过渡,Vue 会自动检测过渡何时完成。 但如果你的动画完全由 JavaScript 控制,你就需要手动调用done
。enterCancelled
和leaveCancelled
: 当过渡被中断的时候会调用。
第二部分:<TransitionGroup>
组件:团队作战,整齐划一
<TransitionGroup>
组件用于多个元素或组件的过渡效果。 想象一下,一个列表的元素逐个淡入,或者一个网格的元素以动画形式重新排列,这些都可以用 <TransitionGroup>
来实现。
2.1 基本用法:让你的列表跳起舞来
<template>
<div>
<button @click="addItem">Add Item</button>
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.text }}
<button @click="removeItem(item.id)">Remove</button>
</li>
</TransitionGroup>
</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 - 1}` });
};
const removeItem = (id) => {
items.value = items.value.filter(item => item.id !== id);
};
return { items, addItem, removeItem };
}
};
</script>
<style>
.list-enter-active,
.list-leave-active {
transition: all 1s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-move {
transition: transform 1s ease;
}
</style>
tag
: 指定<TransitionGroup>
渲染成什么 HTML 标签。 默认是<span>
。key
: 每个子元素都必须有一个唯一的key
属性,Vue 用它来跟踪元素的身份,从而正确地应用过渡效果。.list-move
: 这个类名是用来处理元素位置变化的动画。 当列表中的元素重新排序时,Vue 会自动添加这个类名。
2.2 类名切换逻辑:集体舞的队形变换
<TransitionGroup>
组件的类名切换逻辑和 <Transition>
类似,但是多了一个 .v-move
类名。
类名 | 作用 | 添加时机 | 移除时机 |
---|---|---|---|
v-enter-from |
定义进入过渡的开始状态。 | 元素插入DOM之前 | 进入过渡/动画的下一帧(保证浏览器渲染) |
v-enter-active |
定义进入过渡的生效状态。通常在这里设置transition 属性。 |
元素插入DOM之后 | 进入过渡/动画完成时 |
v-enter-to |
定义进入过渡的结束状态。 | 进入过渡/动画的下一帧(保证浏览器渲染) | 进入过渡/动画完成时 |
v-leave-from |
定义离开过渡的开始状态。 | 离开过渡开始时 | 离开过渡/动画的下一帧(保证浏览器渲染) |
v-leave-active |
定义离开过渡的生效状态。通常在这里设置transition 属性。 |
离开过渡开始时 | 离开过渡/动画完成时 |
v-leave-to |
定义离开过渡的结束状态。 | 离开过渡/动画的下一帧(保证浏览器渲染) | 元素从DOM移除时 |
v-move |
定义移动过渡的生效状态。通常在这里设置transition 属性。 |
元素位置改变时 | 移动过渡/动画完成时 |
v-
前缀: 这里的v-
同样可以替换成你通过name
属性指定的名称,比如list-
。
2.3 JavaScript 钩子:团队协作的指挥棒
<TransitionGroup>
组件也支持 JavaScript 钩子,用法和 <Transition>
类似。 只是,这些钩子会被应用到每一个子元素上。
<template>
<div>
<button @click="addItem">Add Item</button>
<TransitionGroup name="list" tag="ul"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
@after-leave="afterLeave"
>
<li v-for="item in items" :key="item.id">
{{ item.text }}
<button @click="removeItem(item.id)">Remove</button>
</li>
</TransitionGroup>
</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 - 1}` });
};
const removeItem = (id) => {
items.value = items.value.filter(item => item.id !== id);
};
const beforeEnter = (el) => {
console.log('beforeEnter', el);
};
const enter = (el, done) => {
console.log('enter', el);
void el.offsetWidth;
el.classList.add('list-enter-active');
el.addEventListener('transitionend', done);
};
const afterEnter = (el) => {
console.log('afterEnter', el);
el.classList.remove('list-enter-active');
};
const leave = (el, done) => {
console.log('leave', el);
void el.offsetWidth;
el.classList.add('list-leave-active');
el.addEventListener('transitionend', done);
};
const afterLeave = (el) => {
console.log('afterLeave', el);
el.classList.remove('list-leave-active');
};
return { items, addItem, removeItem, beforeEnter, enter, afterEnter, leave, afterLeave };
}
};
</script>
<style>
.list-enter-active,
.list-leave-active {
transition: all 1s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-move {
transition: transform 1s ease;
}
</style>
第三部分: 渲染器如何处理这些逻辑? 源码揭秘!
好了,说了这么多用法,让我们稍微深入一点,看看 Vue 3 的渲染器是如何处理这些动画钩子和类名切换的。
3.1 Transition
组件的渲染
<Transition>
组件本质上是一个函数式组件,它的 render
函数会根据组件的状态(进入、离开)来添加和移除相应的CSS类名,并触发相应的JavaScript钩子。
简化后的伪代码如下:
function renderTransition(props, { slots }) {
const element = slots.default(); // 获取被包裹的元素
if (isEntering) {
// 进入过渡
addClass(element, `${props.name}-enter-from`);
//... 各种逻辑,包括触发 beforeEnter 钩子, requestAnimationFrame 等
addClass(element, `${props.name}-enter-active`);
addClass(element, `${props.name}-enter-to`);
onTransitionEnd(() => {
removeClass(element, `${props.name}-enter-active`);
removeClass(element, `${props.name}-enter-to`);
//... 触发 afterEnter 钩子
});
} else if (isLeaving) {
// 离开过渡
addClass(element, `${props.name}-leave-from`);
//... 各种逻辑,包括触发 beforeLeave 钩子
addClass(element, `${props.name}-leave-active`);
addClass(element, `${props.name}-leave-to`);
onTransitionEnd(() => {
removeClass(element, `${props.name}-leave-active`);
removeClass(element, `${props.name}-leave-to`);
//... 触发 afterLeave 钩子,并且移除元素
});
}
return element;
}
addClass
和removeClass
: 这些函数用于添加和移除CSS类名。 它们会处理浏览器兼容性问题。onTransitionEnd
: 这是一个辅助函数,用于监听transitionend
事件。 当过渡完成时,它会执行回调函数。requestAnimationFrame
: 在添加*-enter-to
类名之前,Vue 会使用requestAnimationFrame
来强制浏览器重绘。 这样可以确保初始样式被应用,从而触发过渡效果。
3.2 TransitionGroup
组件的渲染
<TransitionGroup>
组件的渲染逻辑稍微复杂一些,因为它需要处理多个子元素的过渡效果。
简化后的伪代码如下:
function renderTransitionGroup(props, { slots }) {
const children = slots.default(); // 获取所有子元素
// 遍历子元素,对每个元素应用过渡效果
children.forEach(child => {
if (isEntering) {
// 进入过渡
//... 和 Transition 组件类似,添加 enter 相关类名,触发 enter 钩子
} else if (isLeaving) {
// 离开过渡
//... 和 Transition 组件类似,添加 leave 相关类名,触发 leave 钩子
}
// 处理移动过渡
if (child.isMoved) {
addClass(child, `${props.name}-move`);
onTransitionEnd(() => {
removeClass(child, `${props.name}-move`);
});
}
});
return h(props.tag, children); // 渲染成指定的 HTML 标签
}
child.isMoved
: 这个属性表示子元素的位置是否发生了改变。 Vue 会通过比较新旧 VNode 树来检测元素的位置变化。h
: 这是 Vue 3 的createElement
函数,用于创建 VNode。
第四部分: 一些 Tips 和注意事项
appear
属性:<Transition>
和<TransitionGroup>
都支持appear
属性。 如果设置了appear
属性,组件会在初始渲染时也执行进入过渡。mode
属性:<Transition>
组件支持mode
属性,可以设置为in-out
或out-in
,用来控制进入和离开过渡的顺序。- 性能优化: 过渡效果可能会影响性能,特别是当元素数量很多时。 可以考虑使用 CSS 硬件加速,或者使用 JavaScript 动画库来优化性能。
- 动画库: 如果你想使用更复杂的动画效果,可以考虑使用一些流行的 JavaScript 动画库,比如 GreenSock (GSAP) 或 Anime.js。
总结:掌握动画的魔法
好了,今晚的讲座就到这里。 希望大家通过今天的学习,能够对 Vue 3 的 <Transition>
和 <TransitionGroup>
组件有更深入的理解。 掌握了这些知识,你就可以像魔法师一样,给你的 Web 应用添加各种各样的动画效果,让它们更加生动有趣!
下次再见!