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状态切换,这只涉及布尔值true和false。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;否则使用逻辑取反在true和false之间切换。 - 事件通知:通过
bus.emit将visible状态变化通知父组件。
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;
};
onMounted和onUnmounted:我们在组件挂载时通过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>


7950

被折叠的 条评论
为什么被折叠?



