从零构建 Vue 3 递归树形组件:功能实现与核心要点详解在 Vue 3 开发中,树形组件是一种常见的组件形式,常用于展
在 Vue 3 开发中,树形组件是一种常见的组件形式,常用于展示层次结构的数据,如文件夹系统、组织架构等。在构建递归树形组件时,我们不仅要处理节点的递归渲染,还需要管理节点的展开/折叠、显示/隐藏等状态变化。
本文将从零开始,深入讲解如何使用 Vue 3 构建一个支持 递归渲染、节点状态管理、事件总线通信 的树形组件。本文还会详细介绍如何处理节点的 expanded
和 visible
状态,以及如何通过 事件总线 实现父子组件的高效通信。
功能概述
我们将实现以下功能:
- 递归渲染节点:树形结构中的每个节点可以包含子节点,通过递归方式渲染节点。
- 展开/折叠功能:每个节点可以独立展开或折叠,控制子节点的显示或隐藏。
- 显示/隐藏状态:支持节点的
visible
属性,允许三种状态:true
(显示)、false
(隐藏)、null
(部分选中状态)。 - 事件总线通信:通过
mitt
实现父子组件之间的通信,避免直接事件绑定带来的复杂性。 - 事件监听器的销毁:在组件销毁时,销毁事件监听器以防止内存泄漏。
关键代码讲解
1. 递归渲染节点
递归渲染是树形组件的核心。每个节点可以包含子节点,递归地调用自身组件来渲染子节点。
代码片段:递归渲染
<template>
<div class="tree-node">
<div class="node-content">
<!-- 展开/折叠按钮 -->
<button v-if="node.children && node.children.length > 0" @click="toggleExpand">
{{ node.expanded ? '-' : '+' }}
</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. 展开/折叠功能
控制节点的展开与折叠状态是树形组件的基础功能。我们使用 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
:通过事件总线发射事件,通知父组件节点的展开/折叠状态发生变化。父组件捕获该事件后可做进一步处理。
3. 显示/隐藏状态管理
visible
属性用于控制节点的显示和隐藏状态,支持 true
(显示)、false
(隐藏)、null
(部分选中状态)。我们通过复选框来切换 visible
的状态。
代码片段:控制 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
状态变化通知父组件。
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
:销毁事件监听器是防止内存泄漏的关键一步,确保当组件被销毁时,不再响应事件总线上的事件。
完整代码实现
1. 父组件 TreeComponent.vue
<template>
<div class="tree">
<!-- 递归渲染节点 -->
<TreeNode
v-for="(node, index) in treeData"
:key="node.id"
:node="node"
/>
</div>
</template>
<script setup>
import TreeNode from './TreeNode.vue';
import { reactive, onMounted, onUnmounted } from 'vue';
import bus from './bus'; // 导入事件总线
// 将 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>
2. 子组件 TreeNode.vue
<template>
<div class="tree-node">
<div class="node-content">
<!-- 展开/折叠按钮 -->
<button v-if="node.children && node.children.length > 0" @click="toggleExpand">
{{ node.expanded ? '-' : '+' }}
</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;
}
input[type="checkbox"] {
margin-right: 10px;
}
</style>
3. 事件总线 bus.js
import mitt from 'mitt';
const bus = mitt(); // 创建事件总线
export default bus;
4. 初始化数据 treeData.js
// 示例树结构
export default [
{
id: 1,
name: 'Root',
visible: true,
expanded: true,
children: [
{
id: 2,
name: 'Child 1',
visible: true,
expanded: false,
children: [
{ id: 3, name: 'Grandchild 1', visible: true, expanded: false, children: [] },
{ id: 4, name: 'Grandchild 2', visible: false, expanded: false, children: [] }
]
},
{ id: 5, name: 'Child 2', visible: true, expanded: true, children: [] }
]
}
]
总结
本文展示了如何在 Vue 3 中构建一个功能完备的递归树形组件,涵盖了递归渲染、节点状态管理以及事件总线的应用。重点介绍了如何通过 mitt
实现父子组件的高效通信,并在组件销毁时清理事件监听器,防止内存泄漏。此外,expanded
和 visible
状态的处理逻辑,使得组件能够灵活应对多层次的数据结构。
通过这些实现,你可以为项目中的树形数据展示提供强大的支持,同时保证组件的性能和可维护性。希望这篇文章为你构建复杂的递归组件提供思路和启发。
转载自:https://juejin.cn/post/7414733004885311523