你到底懂不懂 Transition 组件?

简介: 你到底懂不懂 Transition 组件?

image.png


前言

欢迎关注同名公众号《熊的猫》,文章会同步更新!

<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-ifv-for 一起使用时,在 Vue2Vue3 中的不同

  • Vue2 中,当它们处于同一节点时,v-for 的优先级比 v-if 更高,即 v-if 将分别重复运行于每个 v-for 循环中,也就是 v-if 可以正常访问 v-for 中的数据
  • Vue3 中,当它们处于同一节点时,v-if 的优先级比 v-for 更高,即此时只要 v-if 的值为 falsev-for 的列表就不会被渲染,也就是 v-if 不能访问到 v-for 中的数据

六个过渡时机

image.png

总结起来就分为 进入离开 动画的 初始状态、生效状态、结束状态,具体如下:

  • 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> 组件传一个 nameprop 来声明一个过渡效果名,如下就是将 v 前缀修改为 modal 前缀:

<Transition name="modal"> ... </Transition>
复制代码

Transition 组件 & CSS transition 属性

image.png

以上这个简单的效果,核心就是两个时机:

  • v-enter-active 进入动画的 生效状态
  • v-leave-active 离开动画的 生效状态

再配合简单的 CSS 过渡属性就可以达到效果,代码如下:

<template>
  <div class="home">
    <transition name="golden">
      <!-- 金子列表 -->
      <div class="golden-box" v-show="show">
        <img
          class="golden"
          :key="idx"
          v-for="idx in 3"
          src="../assets/golden.jpg"
        />
      </div>
    </transition>
  </div>
  <!-- 钱袋子 -->
  <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>
复制代码

当然动画的效果是多种多样的,不仅只是局限于这一种,例如可以配合:

  • CSStransition 过渡属性(上述例子使用的方案)
  • CSSanimation 动画属性
  • gsap 库

核心原理

通过上述内容其实不难发现其核心原理就是:

  • 组件(DOM)挂载 时,将过渡动效添加到该 DOM 元素上
  • 组件(DOM)卸载 时,不是直接卸载,而是等待附加到 DOM 元素上的 动效执行完成,然后在真正执行卸载操作,即 延迟卸载时机

在上述的过程中,<Transition> 组件会为 目标组件/元素 通过添加不同的 class 来定义 初始、生效、结束 三个状态,当进入下一个状态时会把上一个状态对应的 class 移除。


那么你可能会问了,v-show 的形式也不符合 挂载/卸载 的形式呀,毕竟它只是在修改 DOM 元素的 display: none | x 的样式!

让源码中的注释来回答:

image.png

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");
      });
    });
  });
复制代码

以上就是 进场动效 的实现,如下:

image.png

离场动效

有了进场动效的实现过程,在定义 离场动效 时就可以选择和 进场动效 相对应的形式,即 初始状态过渡过程结束状态

用 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();
        });
      });
    });
  });
复制代码

离场动效,如下:

image.png

实现 Transition 组件

以上的实现过程,可以将其进行抽象化为三个阶段:

  • beforeEnter
  • enter
  • leave

image.png

现在要从 命令式编程 转向 声明式编程 了,因为我们要去编写 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
      return innerVNode
    }
  }
})
</script>
复制代码

最后

从整体来看,Transition 组件 的核心并不算复杂,特别是以 命令式编程 实现之后,但话说回来在 Vue 源码中实现的还是很全面的,比如:

  • 提供 props 实现用户自定义类名
  • 提供 内置模式,即先进后出(in-out)、后进先出(enter-to
  • 支持 v-show 方式触发过渡效果

目录
相关文章
|
5月前
|
前端开发 搜索推荐 测试技术
作业:定制化UI界面
本文介绍如何基于若依(RuoYi)框架定制项目UI,包括更换浏览器标签页logo与标题、系统页面logo、登录页名称及背景图,去除官网标识,并调整主题风格。通过替换`favicon.ico`、修改`index.html`和环境配置文件、更新`logo.png`、编辑`login.vue`组件,以及在`Navbar.vue`中删除相关链接,实现项目个性化。同时,可通过`setting.js`和`settings.js`调整布局与主题色,提升项目专业度与品牌统一性。
|
5月前
|
SQL 人工智能 分布式计算
【MaxCompute SQL AI 实操教程】0元体验使用大模型提效数据分析
【MaxCompute SQL AI 实操教程】0元体验使用大模型提效数据分析
779 4
|
JSON 前端开发 安全
前端开发中的跨域问题及解决方案
在前端开发中,跨域是一个常见但又令人头疼的问题。本文将深入探讨跨域产生的原因以及一些常见的解决方案,帮助开发者更好地理解和处理跨域情况。
|
前端开发 JavaScript 大数据
关于JavaScript性能问题的误解
JavaScript 是单线程语言,代码逐行执行,遇到大数据量计算可能影响性能。前端同事担心遍历大量数据会导致性能问题,但实际上,即使遍历1000、10000条数据,耗时也较少。测试代码执行时间有三种方法:Date.now、console.time 和 performance.now,其中 performance.now 精度最高。开发中不必过度担忧遍历带来的性能损耗,保持代码清晰更重要。
|
存储 前端开发 JavaScript
面试官问:如果有100个请求,你如何使用Promise控制并发?
面试官问:如果有100个请求,你如何使用Promise控制并发?
1121 0
|
机器学习/深度学习 PyTorch 算法框架/工具
彻底告别微调噩梦:手把手教你击退灾难性遗忘,让模型记忆永不褪色的秘密武器!
【10月更文挑战第5天】深度学习中,模型微调虽能提升性能,但也常导致灾难性遗忘,即学习新任务时遗忘旧知识。本文介绍几种有效解决方案,重点讲解弹性权重巩固(EWC)方法,通过在损失函数中添加正则项来防止重要权重被更新,保护模型记忆。文中提供了基于PyTorch的代码示例,包括构建神经网络、计算Fisher信息矩阵和带EWC正则化的训练过程。此外,还介绍了其他缓解灾难性遗忘的方法,如LwF、在线记忆回放及多任务学习,以适应不同应用场景。
1937 8
|
前端开发 JavaScript
js 进入浏览器全屏(F11效果)、退出全屏、指定元素全屏、判断当前是否全屏、监听浏览器全屏事件、定义全屏时的css样式(全屏伪类)
js 进入浏览器全屏(F11效果)、退出全屏、指定元素全屏、判断当前是否全屏、监听浏览器全屏事件、定义全屏时的css样式(全屏伪类)
1458 0
|
前端开发 JavaScript Java
使用Spring Boot实现跨域资源共享(CORS)
使用Spring Boot实现跨域资源共享(CORS)
|
缓存 Java 微服务
Spring Boot中的跨域请求处理
Spring Boot中的跨域请求处理
|
安全 Cloud Native Linux
Linux命令(123)之mail
Linux命令(123)之mail
279 1