Vue3封装树形组件

1.功能概述

我们需要实现的树形组件大致如下:
在这里插入图片描述
我将实现以下功能:

  • 递归渲染节点:树形结构中的每个节点可以包含子节点,通过递归方式渲染节点。
  • 展开/折叠功能:每个节点可以独立展开或折叠,控制子节点的显示或隐藏。
  • 显示/隐藏状态:支持节点的visible属性,允许三种状态:true(显示)、false(隐藏)、null(部分选中状态)。
  • 事件总线通信:通过mitt实现父子组件之间的通信,避免直接事件绑定带来的复杂性。
  • 事件监听器的销毁:在组件销毁时,销毁事件监听器以防止内存泄漏。

2.关键代码讲解

2.1 递归渲染节点

递归渲染是树形组件的核心。每个节点可以包含子节点,递归地调用自身组件来渲染子节点。

<template>
  <div class="tree-node">
    <div class="node-content">
      <!-- 展开/折叠按钮 -->
      <button>
        //确保只有当前节点有子节点时,才渲染展开/折叠按钮
        <div  v-if="node.children && node.children.length > 0"
              @click="toggleExpand">
          <img class="expanded-img" src="./images/hiden.png" v-show="node.expanded">
          <img class="hiden-img" src="./images/hiden.png" v-show="!node.expanded">
        </div>
      </button>
        <!-- visible 复选框,支持中间态 -->
      <input
        type="checkbox"
        :checked="node.visible === true"
        :indeterminate="node.visible === null"
        @change="toggleVisibility"
      />
      <span>{{ node.name }}</span>
    </div>
    <!-- 递归渲染子节点 -->
    <div v-if="node.expanded" class="children">
      <TreeNode v-for="child in node.children" :key="child.id" :node="child" />
    </div>
  </div>
</template>
  • v-if="node.children":确保只有当前节点有子节点时,才渲染展开/折叠按钮。
  • 递归渲染子节点:每个节点通过TreeNode递归渲染其子节点。Vue 通过v-for确保每个子节点拥有唯一的key,从而避免渲染异常。

2.2 展开/折叠功能

控制节点的展开与折叠状态是树形组件的基础功能。我们使用expanded属性来控制节点的展开状态,并通过点击按钮切换。

const toggleExpand = () => {
  const newExpanded = !props.node.expanded;  // 反转 expanded 状态
  bus.emit('update-expanded', { node: props.node, expanded: newExpanded });
};
  • !props.node.expanded:通过逻辑取反简化expanded状态切换,这只涉及布尔值truefalse
  • bus.emit:通过事件总线发射事件,通知父组件节点的展开/折叠状态发生变化。父组件捕获该事件后可做进一步处理。

2.3 显示/隐藏状态管理

visible属性用于控制节点的显示和隐藏状态,支持true(显示)、false(隐藏)、null(部分选中状态)。我们通过复选框来切换visible的状态。

const toggleVisibility = () => {
  let newVisible;
  if (props.node.visible === null) {
    newVisible = true;  // 从 null 切换到 true
  } else {
    newVisible = !props.node.visible;  // 切换 true <-> false
  }
  bus.emit('toggle-visibility', { node: props.node, visible: newVisible });
};
  • 三种状态的处理:如果visible当前为null,表示复选框是部分选中状态,我们将其设置为true;否则使用逻辑取反在truefalse之间切换。
  • 事件通知:通过bus.emitvisible状态变化通知父组件。

2.4 事件总线的使用与销毁

为了避免事件冒泡引发的复杂性,组件使用了mitt事件总线进行状态通信。同时,为了防止内存泄漏,我们需要在组件销毁时同步销毁事件监听器。

import { onMounted, onUnmounted } from 'vue';
import bus from './bus';  // 导入事件总线onMounted(() => {
  bus.on('toggle-visibility', handleVisibilityChange);
  bus.on('update-expanded', handleExpandChange);
});onUnmounted(() => {
  bus.off('toggle-visibility', handleVisibilityChange);
  bus.off('update-expanded', handleExpandChange);
});const handleVisibilityChange = ({ node, visible }) => {
  node.visible = visible;
  updateParentVisibility(node);
  updateChildrenVisibility(node, visible);
};const handleExpandChange = ({ node, expanded }) => {
  node.expanded = expanded;
};
  • onMountedonUnmounted:我们在组件挂载时通过onMounted添加事件监听器,并在组件销毁时通过onUnmounted移除这些监听器,防止内存泄漏。
  • bus.off:销毁事件监听器是防止内存泄漏的关键一步,确保当组件被销毁时,不再响应事件总线上的事件。

3.完整代码

目录如下:
在这里插入图片描述

子组件 tree-node.vue:

<template>
  <div class="tree-node">
    <div class="node-content">
      <!-- 展开/折叠按钮 -->
      <button>
        <div
          v-if="node.children && node.children.length > 0"
          @click="toggleExpand"
        >
          <img
            class="expanded-img"
            src="./images/hiden.png"
            v-show="node.expanded"
          />
          <img
            class="hiden-img"
            src="./images/hiden.png"
            v-show="!node.expanded"
          />
        </div>
      </button>
      <!-- visible 复选框,支持中间态 -->
      <input
        type="checkbox"
        :checked="node.visible === true"
        :indeterminate="node.visible === null"
        @change="toggleVisibility"
      />
      <span>{{ node.name }}</span>
    </div>
    <!-- 递归渲染子节点 -->
    <div v-if="node.expanded" class="children">
      <TreeNode v-for="child in node.children" :key="child.id" :node="child" />
    </div>
  </div>
</template>
<script setup>
import bus from "./bus";
const props = defineProps({
  node: {
    type: Object,
    required: true,
  },
});
// 切换当前节点的展开/折叠状态
const toggleExpand = () => {
  const newExpanded = !props.node.expanded; // 简化 expanded 处理
  bus.emit("update-expanded", { node: props.node, expanded: newExpanded });
};
// 切换当前节点的显示状态
const toggleVisibility = () => {
  let newVisible;
  if (props.node.visible === null) {
    newVisible = true; // 如果是 null,默认改为 true
  } else {
    newVisible = !props.node.visible; // 切换 true <-> false
  }
  bus.emit("toggle-visibility", { node: props.node, visible: newVisible });
};
</script>
<style scoped>
.tree-node {
  margin-left: 20px;
}
.node-content {
  display: flex;
  align-items: center;
}
button {
  margin-right: 5px;
  background-color: #ffffff;
  width: 20px;
  height: 20px;
  border: 0;
}
button > div {
  width: 20px;
  height: 20px;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
}
button img {
  height: 20px;
}
.expanded-img {
  transform: rotate(90deg);
}
input[type="checkbox"] {
  margin-right: 10px;
}
</style>

父组件 tree-component.vue:

<template>
  <div class="tree">
    <!-- 递归渲染节点 -->
    <TreeNode v-for="(node, index) in treeData" :key="node.id" :node="node" />  
  </div>
</template>
<script setup>
import TreeNode from "./tree-node.vue";
import { reactive, onMounted, onUnmounted } from "vue";
import bus from "./bus.js"; // 导入事件总线
// 将 treeData 包装为响应式对象
const props = defineProps({
  treeData: {
    type: Array,
    required: true,
  },
});
const treeData = reactive([...props.treeData]); // 确保 treeData 是响应式的

// 递归设置 parent(初始化时调用)
const setParentReference = (nodes, parent = null) => {
  nodes.forEach((node) => {
    node.parent = parent;
    if (node.children) {
      setParentReference(node.children, node);
    }
  });
};
// 初始化时设置 parent 属性
setParentReference(treeData);

const handleVisibilityChange = ({ node, visible }) => {
  node.visible = visible;
  updateParentVisibility(node); // 更新父节点状态
  updateChildrenVisibility(node, visible); // 更新子节点状态
};
const handleExpandChange = ({ node, expanded }) => {
  node.expanded = expanded;
};

// 组件挂载时添加事件监听器,销毁时移除监听器
onMounted(() => {
  bus.on("toggle-visibility", handleVisibilityChange);
  bus.on("update-expanded", handleExpandChange);
});
onUnmounted(() => {
  bus.off("toggle-visibility", handleVisibilityChange);
  bus.off("update-expanded", handleExpandChange);
});

// 递归更新父节点的可见状态
const updateParentVisibility = (node) => {
  if (node.parent) {
    const allVisible = node.parent.children.every(
      (child) => child.visible === true
    );
    const allHidden = node.parent.children.every(
      (child) => child.visible === false
    );
    if (allVisible) {
      node.parent.visible = true;
    } else if (allHidden) {
      node.parent.visible = false;
    } else {
      node.parent.visible = null; // 中间状态
    }
    updateParentVisibility(node.parent); // 递归向上更新父节点
  }
};
// 递归更新子节点的可见状态
const updateChildrenVisibility = (node, visible) => {
  if (node.children && node.children.length > 0) {
    node.children.forEach((child) => {
      child.visible = visible;
      updateChildrenVisibility(child, visible); // 递归向下更新子节点
    });
  }
};
</script><style scoped>
.tree {
   padding-left: 10px;
}
</style>

事件总线 bus.js:

//需要先安装mitt,npm install mitt
import mitt from 'mitt';
const bus = mitt();  // 创建事件总线
export default bus;

初始化数据 treeData.js:

<template>
  <treeComps :treeData="trees"></treeComps>
</template>
<script setup>
import treeComps from "../lib/tree/src/tree-component.vue";
const trees = [
  {
    id: 1,
    name: "Root",
    visible: true,
    expanded: true,
    children: [
      {
        id: 2,
        name: "Child 1",
        visible: true,
        expanded: true,
        children: [
          {
            id: 3,
            name: "Grandchild 1",
            visible: false,
            expanded: false,
            children: [],
          },
          {
            id: 4,
            name: "Grandchild 2",
            visible: false,
            expanded: true,
            children: [
              {
                id: 5,
                name: "node 5",
                visible: true,
                expanded: true,
                children: [],
              },
            ],
          },
        ],
      },
      { id: 6, name: "Child 2", visible: true, expanded: true, children: [] },
    ],
  },
];
</script>
<style scoped>
html,
body {
  margin: 0;
  padding: 0;
}
</style>

详细代码见https://gitee.com/wlmProject/custom-tree-component

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

太阳与星辰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值