你到底懂不懂 Transition 组件?
前言
欢迎关注同名公众号《 熊的猫 》,文章会同步更新!
<Transition>
作为一个
Vue
中的内置组件,它可以将
进入动画
和
离开动画
应用到通过
默认插槽
传递给目标元素或组件上。
也许你有在使用,但是一直不清楚它的原理或具体实现,甚至不清楚其内部提供的各个 class 到底怎么配合使用,想看源码又被其中各种引入搞得七荤八素...
本篇文章就以 Transition 组件为核心,探讨其核心原理的实现,文中不会对其各个属性再做额外解释,毕竟这些看文档就够了, 希望能够给你带来帮助!!!
Transition 内置组件
触发条件
<Transition>
组件的
进入动画
或
离开动画
可通过以下的条件之一触发:
-
由
v-if
所触发的切换 -
由
v-show
所触发的切换 -
由特殊元素
<component name="x">
切换的动态组件 -
改变特殊的
key
属性
再分类
其实我们可以将以上情况进行 再分类 :
- 组件 挂载 和 销毁
-
v-if
的变化 -
<component name="x">
的变化 -
key
的变化 -
组件
样式
属性
display: none | x
设置 -
v-show
的变化
【 扩展 】v-if
和v-for
一起使用时,在Vue2
和Vue3
中的不同
-
在
Vue2
中,当它们处于同一节点时,
v-for
的优先级比v-if
更高,即v-if
将分别重复运行于每个v-for
循环中,也就是v-if
可以正常访问v-for
中的数据 -
在
Vue3
中,当它们处于同一节点时,
v-if
的优先级比v-for
更高,即此时只要v-if
的值为false
则 v-for 的列表就不会被渲染,也就是v-if
不能访问到v-for
中的数据
六个过渡时机
总结起来就分为 进入 和 离开 动画的 初始状态、生效状态、结束状态 ,具体如下:
-
v-enter-from
- 进入 动画的 起始状态
- 在元素插入之前添加,在元素插入完成后的 下一帧移除
-
v-enter-active
- 进入 动画的 生效状态 ,应用于整个进入动画阶段
- 在元素被插入之前添加, 在过渡或动画完成之后移除
-
这个
class
可以被用来定义进入动画的持续时间、延迟与速度曲线类型
-
v-enter-to
- 进入 动画的 结束状态
-
在元素插入完成后的下一帧被添加 (也就是
v-enter-from
被移除的同时), 在过渡或动画完成之后移除 -
v-leave-from
- 离开 动画的 起始状态
- 在离开过渡效果被触发时立即添加, 在一帧后被移除
-
v-leave-active
- 离开 动画的 生效状态 ,应用于整个离开动画阶段
- 在离开过渡效果被触发时立即添加,在 过渡或动画完成之后移除
-
这个
class
可以被用来定义离开动画的持续时间、延迟与速度曲线类型 -
v-leave-to
- 离开 动画的 结束状态
-
在一个离开动画被触发后的
下一帧
被添加 (即
v-leave-from
被移除的同时),在 过渡或动画完成之后移除
其中的
v
前缀是允许修改的,可以
<Transition>
组件传一个
name
的
prop
来声明一个过渡效果名,如下就是将
v
前缀修改为
modal
前缀:
<Transition name="modal"> ... </Transition>
Transition 组件 & CSS transition 属性
以上这个简单的效果,核心就是两个时机:
-
v-enter-active
进入动画的 生效状态 -
v-leave-active
离开动画的 生效状态
再配合简单的 CSS 过渡属性就可以达到效果,代码如下:
<template>
<div class="home">
<transition name="golden">
<!-- 金子列表 -->
<div class="golden-box" v-show="show">
class="golden"
:key="idx"
v-for="idx in 3"
src="../assets/golden.jpg"
</transition>
<!-- 钱袋子 -->
<img class="purse" @click="show = !show" src="../assets/purse.png" alt="" />
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
const show = ref(true)
</script>
<style lang="less" scoped>
.home {
min-height: 66px;
.golden-box {
transition: all 1s ease-in;
.golden {
width: 100px;
position: fixed;
transform: translate3d(0, 0, 0);
transition: all .4s;
&:nth-of-type(1) {
left: 45%;
top: 100px;
&:nth-of-type(2) {
left: 54%;
top: 50px;
&:nth-of-type(3) {
right: 30%;
top: 100px;
&.golden-enter-active {
.golden {
transform: translate3d(0, 0, 0);
transition-timing-function: cubic-bezier(0, 0.57, 0.44, 1.97);
.golden:nth-of-type(1) {
transition-delay: 0.1s;
.golden:nth-of-type(2) {
transition-delay: 0.2s;
.golden:nth-of-type(3) {
transition-delay: 0.3s;
&.golden-leave-active {
.golden:nth-of-type(1) {
transform: translate3d(150px, 140px, 0);
transition-delay: 0.3s;
.golden:nth-of-type(2) {
transform: translate3d(0, 140px, 0);
transition-delay: 0.2s;
.golden:nth-of-type(3) {
transform: translate3d(-100px, 140px, 0);
transition-delay: 0.1s;
.purse {
position: fixed;
width: 200px;
margin-top: 100px;
cursor: pointer;
</style>
当然动画的效果是多种多样的,不仅只是局限于这一种,例如可以配合: - CSS 的 transition 过渡属性(上述例子使用的方案) - CSS 的 animation 动画属性 - f=" https://www. npmjs.com/package/gsap ">gsap 库
核心原理
通过上述内容其实不难发现其核心原理就是:
- 当 组件(DOM) 被 挂载 时,将过渡动效添加到该 DOM 元素上
- 当 组件(DOM) 被 卸载 时, 不是直接卸载 ,而是等待附加到 DOM 元素上的 动效执行完成 ,然后在真正执行卸载操作,即 延迟卸载时机
在上述的过程中,
<Transition>
组件会为
目标组件/元素
通过添加不同的
class
来定义
初始、生效、结束
三个状态,当进入下一个状态时会把上一个状态对应的
class
移除。
那么你可能会问了,
v-show
的形式也不符合
挂载/卸载
的形式呀,毕竟它只是在修改
DOM
元素的
display: none | x
的样式!
让源码中的注释来回答:
v-if
、
<component name="x">
、
key
控制组件
显示/隐藏
的方式是
挂载/卸载
组件,而
v-show
控制组件
显示/隐藏
的方式是
修改/重置
display: none | x
属性值,从本质上看方式不同,但从结果上看都属于控制组件的
显示/隐藏
,即功能是一致的,而这里所说的
挂载/卸载
是针对大部分情况来说的,毕竟四种触发方式中就有三种符合此情况。
实现 Transition 组件
所谓 Transition 组件毕竟是 Vue 的内置组件,换句话说,组件的编写要符合 Vue 的规范(即 声明式写法 ),但为了更好的理解核心原理,我们应该从 原生 DOM 的过渡开始(即 命令式写法 )探讨。
原生 DOM 如何实现过渡?
所谓的 过渡动效 本质上就是一个 DOM 元素在 两种状态间的转换 , 浏览器 会根据我们设置的过渡效果 自行完成 DOM 元素的过渡 。
而
状态的转换
指的就是
初始化状态
和
结束状态
的转换,并且配合 CSS 中的
transition
属性就可以实现两个状态间的过渡,即
运动过程
。
原生 DOM 元素移动示例
假设要为一个元素在垂直方向上添加进场动效:从 原始位置 向上移动 200px 的位置,然后在 1s 内运动回 原始位置 。
进场动效
用 CSS 描述
// 描述物体
.box {
width: 100px;
height: 100px;
background-color: red;
box-shadow: 0 0 8px;
border-radius: 50%;
// 初始状态
.enter-from {
transform: translateY(-200px);
// 运动过程
.enter-active {
transition: transform 1s ease-in-out;
// 结束状态
.enter-to {
transform: translateY(0);
}
用 JavaScript 描述
// 创建元素
const div = document.createElement('div')
div.classList.add('box')
// 添加 初始状态 和 运动过程
div.classList.add('enter-from')
div.classList.add('enter-active')
// 将元素添加到页面上
document.body.appendChild(div)
// 切换元素状态
div.classList.remove('enter-from')
div.classList.add('enter-to')
从
命令式编程
的步骤上来看,似乎每一步都没有问题,但实际的过渡动画是不会生效的,虽然在代码中我们有
状态的切换
,但这个切换的操作对于
浏览器
来讲是在
同一帧
中进行的,所以只会渲染
最终状态
,即
enter-to
类所指向的状态。
requestAnimationFrame 实现下一帧的变化
window.requestAnimationFrame(callback)
会在浏览器在
下次重绘之前
调用指定的
回调函数
用于更新动画。
也就是说,单个的 requestAnimationFrame() 方法是在 当前帧 中执行的,也就是如果想要在 下一帧 中执行就需要使用两个 requestAnimationFrame() 方法嵌套的方式来实现,如下:
// 嵌套的 requestAnimationFrame 实现在下一帧中,切换元素状态
requestAnimationFrame(() => {
requestAnimationFrame(() => {
div.classList.remove("enter-from");
div.classList.add("enter-to");
transitionend 事件监听动效结束
以上就完成元素的 进入动效 ,那么在动效结束之后,别忘了将原本和 进入动效 相关的 类 移除掉,可以通过 transitionend 事件 监听动效是否结束,如下
// 嵌套的 requestAnimationFrame 实现在下一帧中,切换元素状态
requestAnimationFrame(() => {
requestAnimationFrame(() => {
div.classList.remove("enter-from");
div.classList.add("enter-to");
// 动效结束后,移除和动效相关的类
div.addEventListener("transitionend", () => {
div.classList.remove("enter-to");
div.classList.remove("enter-active");
以上就是
进场动效
的实现,如下:
离场动效
有了进场动效的实现过程,在定义 离场动效 时就可以选择和 进场动效 相对应的形式,即 初始状态 、 过渡过程 、 结束状态 。
用 CSS 描述
// 初始状态
.leave-from {
transform: translateY(0);
// 过渡状态
.leave-active {
transition: transform 2s ease-out;
// 结束状态
.leave-to {
transform: translateY(-300px);
用 JavaScript 描述
所谓的 离场 就是指 DOM 元素 的 卸载 ,但因为要有离场动效要展示,所以不能直接卸载对应的元素,而是要 等待离场动效结束之后在进行卸载 。
为了直观一些,我们可以添加一个离场的按钮,用于触发离场动效。
// 创建离场按钮
const btn = document.createElement("button");
btn.innerText = "离场";
document.body.appendChild(btn);
// 绑定事件
btn.addEventListener("click", () => {
// 设置离场 初始状态 和 运动过程
div.classList.add("leave-from");
div.classList.add("leave-active");
// 嵌套的 requestAnimationFrame 实现在下一帧中,切换元素状态
requestAnimationFrame(() => {
requestAnimationFrame(() => {
div.classList.remove("leave-from");
div.classList.add("leave-to");
// 动效结束后,移除和动效相关的类
div.addEventListener("transitionend", () => {
div.classList.remove("leave-to");
div.classList.remove("leave-active");
// 离场动效结束,移除目标元素
div.remove();
离场动效,如下:
实现 Transition 组件
以上的实现过程,可以将其进行抽象化为三个阶段: - beforeEnter - enter - leave
现在要从
命令式编程
转向
声明式编程
了,因为我们要去编写
Vue 组件
了,即基于
VNode
节点来实现,为了和普通的
VNode
作为区分,
Vue
中会为目标元素的
VNode
节点上添加
transition
属性: -
Transition 组件
本身不会渲染任何额外的内容,它只是通过
默认插槽
读取过渡元素,并渲染需要过渡的元素 -
Transition 组件
作用,是在过渡元素的
VNode
节点上添加和
transition
相关的
钩子函数
<script lang="ts">
import { defineComponent } from 'vue';
const nextFrame = (callback: () => unknown) => {
requestAnimationFrame(() => {
requestAnimationFrame(callback)
export default defineComponent({
name: 'Transition',
setup(props, { slots }) {
// 返回 render 函数
return () => {
// 通过默认插槽,获取目标元素
const innerVNode = (slots as any).default()
// 为目标元素添加 transition 相关钩子
innerVNode.transition = {
beforeEnter(el: any) {
console.log(111)
// 设置 初始状态 和 运动过程
el.classList.add("enter-from");
el.classList.add("enter-active");
enter(el: any) {
// 在下一帧切换状态
nextFrame(() => {
// 切换状态
el.classList.remove("enter-from");
el.classList.add("enter-to");
// 动效结束后,移除和动效相关的类
el.addEventListener("transitionend", () => {
el.classList.remove("enter-to");
el.classList.remove("enter-active");
leave(el: any) {
// 设置离场 初始状态 和 运动过程
el.classList.add("leave-from");
el.classList.add("leave-active");
// 在下一帧中,切换元素状态
nextFrame(() => {
// 切换元素状态
el.classList.remove("leave-from");
el.classList.add("leave-to");
// 动效结束后,移除和动效相关的类
el.addEventListener("transitionend", () => {
el.classList.remove("leave-to");
el.classList.remove("leave-active");
// 离场动效结束,移除目标元素
el.remove();
// 返回修改过的 VNode