简单记录一个vue3手风琴组件

<!--
  统计面板(通用容器)
  职责:统一每个统计模块的标题/折叠/筛选区/正文布局,
  对应原 mv_statistics.html 中 .bannerbor + .panelDiv 折叠 的结构。
  - title:  面板标题(可点击折叠/展开)
  - note:   红色注释(如下穿/跨越管长来源说明)
  - #filters 具名插槽:筛选下拉/日期等控件
  - 默认插槽:图表 + 表格正文
-->
<template>
  <div class="stat-panel">
    <div class="panel-head">
      <span class="title" @click="collapsed = !collapsed">
        {{ title }}
        <i class="arrow" :class="{ 'arrow--up': !collapsed }"></i>
      </span>
      <div class="filters" @click.stop>
        <slot name="filters"></slot>
      </div>
    </div>
    <div class="panel-body" v-show="!collapsed">
      <div v-if="note" class="note">{{ note }}</div>
      <slot></slot>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { ref } from "vue";

defineProps({
  // 面板标题
  title: {
    type: String,
    default: "",
  },
  // 红色提示语(可选)
  note: {
    type: String,
    default: "",
  },
});

// 是否折叠面板(对应原页面点击 panelDiv 切换显隐)
const collapsed = ref(false);
// 占位:供父级 ResizeObserver 统一调用 sizeChange
const sizeChange = () => {};
defineExpose({ sizeChange });
</script>

<style lang="scss" scoped>
.stat-panel {
  background: #ffffff;
  border-radius: 8px;
  border: 1px solid rgba(0, 0, 0, 0.1);
  box-sizing: border-box;
  & + .stat-panel {
    margin-top: 20px;
  }
  .panel-head {
    display: flex;
    align-items: center;
    padding: 16px 20px;
    .title {
      position: relative;
      flex-shrink: 0;
      color: #161c21;
      font-size: 18px;
      line-height: 24px;
      cursor: pointer;
      user-select: none;
      padding-left: 12px;
      &::before {
        display: block;
        content: "";
        position: absolute;
        left: 0;
        top: 50%;
        width: 4px;
        height: 16px;
        transform: translateY(-50%);
        background: #3d7dff;
      }
      .arrow {
        display: inline-block;
        width: 0;
        height: 0;
        margin-left: 12px;
        border-left: 5px solid transparent;
        border-right: 5px solid transparent;
        border-top: 6px solid #404a53;
        transition: transform 0.2s;
        vertical-align: middle;
      }
      .arrow--up {
        transform: rotate(180deg);
      }
    }
    .filters {
      margin-left: auto;
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 8px;
    }
  }
  .panel-body {
    padding: 0 20px 20px;
    box-sizing: border-box;
  }
  .note {
    color: red;
    font-size: 12px;
    margin-bottom: 10px;
  }
}
</style>

image

 好,现在你已经学会走路了,那么我们接下来就要开始和苏炳添苏神一起比赛百米赛跑了!!!

这个 StatPanel 组件虽然代码量不大,但非常经典,涵盖了 Vue 3 日常开发中大量高频且核心的知识点。我们可以从以下四个维度进行拆解:
1. 组件通信与插槽(Slots)
  具名插槽(Named Slots):
    代码中使用了
<slot name="filters"></slot>。这允许父组件在特定的位置(如面板右上角)注入自定义的筛选控件,而不影响面板的整体结构。   默认插槽(Default Slot):
    <slot></slot> 用于承载面板的核心内容(如图表、表格)。这种“外壳固定,内容自定义”的设计是构建高阶容器组件(Layout Components)的标准做法。   事件冒泡控制:
    在 <div class="filters" @click.stop> 中使用了 .stop 修饰符。因为外层 title 绑定了点击折叠事件,如果不阻止冒泡,用户点击筛选区时也会触发面板的折叠。 2. Vue 3 核心语法(Composition API)   <script setup> 语法糖:
    组件使用了 Vue 3 推荐的编译时语法糖,省去了 setup() 函数的包裹,代码更简洁。   响应式状态:
    使用 ref(
false) 来管理面板的折叠状态 collapsed。   Props 定义:
    使用了 defineProps 来接收父组件传入的 title 和 note,并设置了默认值,保证了组件的健壮性。   暴露方法(defineExpose):
    通过 defineExpose({ sizeChange }) 将组件内部的方法暴露给父组件。结合注释可以看出,这是为了配合父级使用 ResizeObserver 监听尺寸变化时,能够主动调用子组件的刷新逻辑(这在图表类组件中非常常见)。
3. 模板指令与交互   条件渲染 vs 显示隐藏:   v-show="!collapsed" 用于面板正文。因为面板内容可能包含图表等重量级元素,使用 v-show 可以在折叠时隐藏内容,但保留 DOM 节点,避免展开时重新渲染图表导致性能损耗。   v-if="note" 用于红色提示语。因为提示语是轻量级文本,不存在时直接不渲染 DOM 即可。   动态 Class 绑定::class="{ 'arrow--up': !collapsed }" 使用了对象语法,根据状态动态切换 CSS 类名,实现箭头的旋转动画。 4. CSS 布局与样式隔离   Scoped 样式:<style lang="scss" scoped> 确保样式只作用于当前组件,避免污染全局。   Flexbox 弹性布局:.panel-head 使用了 display: flex 和 align-items: center 实现标题和筛选区的水平对齐。   自动右对齐:.filters 使用了 margin-left: auto,这是 Flex 布局中非常巧妙的技巧,无需使用 justify-content: space-between 就能将筛选区推到最右侧。   相邻兄弟选择器:& + .stat-panel { margin-top: 20px; } 表示只有当两个 .stat-panel 紧挨着时,才给后者加上外边距。这避免了给第一个或最后一个面板加多余间距的麻烦。   伪元素装饰:.title::before 使用伪元素在标题左侧画了一个蓝色的竖条,这是一种不增加额外 DOM 节点的纯 CSS 装饰技巧。
上述代码中有时候用v-show,有时候用v-if,从页面渲染流程这里分析给出理由
在 Vue 中,v-if 和 v-show 虽然都能控制元素的显隐,但它们在页面渲染流程和底层实现机制上有着本质的区别。结合你提供的 StatPanel 组件代码,我们可以从以下几个维度进行深度剖析:
1. 底层渲染机制的区别
  v-if(真正的条件渲染):
    它是“惰性”的。在编译阶段,v-if 会被编译为条件分支逻辑。
    当条件为 false 时,该元素及其子组件根本不会被渲染到 DOM 树中,也不会触发子组件的初始化(生命周期钩子不会执行)。
    当条件变为 true 时,Vue 才会重新创建一个全新的 DOM 节点并挂载。   v-show(基于 CSS 的显隐):
    无论条件真假,元素始终会被渲染并保留在 DOM 树中。
    它的底层实现仅仅是切换 CSS 的 display 属性(如 display: none)。
    组件的生命周期钩子在首次渲染时就会全部执行。
2. 结合 StatPanel 代码的具体分析   为什么面板正文(.panel-body)使用 v-show?   <div class="panel-body" v-show="!collapsed">    <slot></slot> <!-- 通常包含图表、复杂表格等重量级内容 -->   </div>   避免昂贵的重复渲染:根据组件注释,插槽内通常承载的是“图表 + 表格正文”。图表库(如 ECharts、VChart)在挂载时需要经历计算布局、绘制 Canvas/SVG 等极其消耗性能的过程。如果使用 v-if,每次用户点击折叠再展开时,图表组件都会被销毁并重新创建,导致明显的卡顿和闪烁。   保留内部状态:使用 v-show 隐藏时,图表的 DOM 节点和内存中的实例依然存在。当用户再次展开时,图表只需重新计算一下尺寸(这也解释了代码中为什么要用 defineExpose 暴露 sizeChange 方法供父级调用),而无需重新请求数据和重绘,用户体验极其流畅。   为什么红色提示语(.note)使用 v-if?     <div v-if="note" class="note">{{ note }}</div>   极低的渲染成本:这仅仅是一个轻量级的纯文本节点。即使条件改变导致 DOM 销毁和重建,对浏览器的性能开销也几乎可以忽略不计。   保持 DOM 树的干净:在绝大多数业务场景中,这种提示语可能并不存在。使用 v-if 可以确保当 note 为空时,DOM 树中不会残留任何无意义的空 <div> 节点,保持了页面结构的整洁。   无需保留状态:提示语不需要像图表那样保留复杂的内部状态,因此不需要 v-show 提供的“常驻内存”特性。 3. 总结与最佳实践   在页面渲染流程的考量下,两者的选择标准非常明确:   用 v-if 的场景:条件在运行时很少改变,或者元素本身非常轻量(如纯文本、简单图标),且不需要保留组件内部状态。   用 v-show 的场景:条件需要频繁切换(如 Tab 切换、折叠面板),或者元素内部包含重量级子组件(如大型表单、图表、复杂列表),为了避免反复销毁和重建带来的性能损耗,必须使用 v-show。

那么看到这里了,就还会引入一个新的东西那就是v-memo

v-memo v-if v-show 三者做一个区别比较
1. 核心定位对比
    v-if (控制“生与死”):真正的条件渲染。负责决定一块 DOM 和组件是否应该存在于页面中。
    v-show (控制“显与隐”):基于 CSS 的显隐切换。负责决定一块已经存在的 DOM 是否对用户可见。
    v-memo (控制“更新与跳过”):渲染缓存指令。负责决定一块已经渲染的 DOM 子树,在父组件更新时是否需要重新执行 diff 算法。

image

 

适用场景总结

✅ 使用 v-if 的场景:
  1. 条件在运行时极少改变(如权限控制、用户登录状态)。
  2. 元素/组件非常轻量,销毁和重建的成本极低。
  3. 需要在条件改变时重置组件内部状态(如重新打开一个表单弹窗,需要清空之前的输入)。
✅ 使用 v-show 的场景:
  1. 需要频繁切换显隐状态(如 Tab 页签切换、下拉菜单、折叠面板)。
  2. 内部包含初始化成本极高的重量级组件(如 ECharts 图表、大型富文本编辑器),避免反复销毁重建导致的卡顿。
✅ 使用 v-memo 的场景:
  1. 超长列表渲染(配合 v-for):列表项大部分时间稳定,只有局部数据改变时,跳过未改变项的 diff 检查。
  2. 复杂的静态子树:某块 DOM 结构极其复杂,且其渲染仅依赖于某几个特定的变量。当父组件其他无关状态更新时,使用 v-memo 保护这块区域不被无效更新。
  3. 防御性优化:在性能瓶颈处,防止不必要的子树重渲染。

posted on 2026-07-23 11:05  中文还在写码  阅读(11)  评论(0)    收藏  举报