likes
comments
collection
share

从零构建 Vue 3 递归树形组件:功能实现与核心要点详解在 Vue 3 开发中,树形组件是一种常见的组件形式,常用于展

作者站长头像
站长
· 阅读数 138

在 Vue 3 开发中,树形组件是一种常见的组件形式,常用于展示层次结构的数据,如文件夹系统、组织架构等。在构建递归树形组件时,我们不仅要处理节点的递归渲染,还需要管理节点的展开/折叠、显示/隐藏等状态变化。

本文将从零开始,深入讲解如何使用 Vue 3 构建一个支持 递归渲染节点状态管理事件总线通信 的树形组件。本文还会详细介绍如何处理节点的 expandedvisible 状态,以及如何通过 事件总线 实现父子组件的高效通信。

功能概述

我们将实现以下功能:

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

关键代码讲解

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 状态切换,这只涉及布尔值 truefalse
  • 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;否则使用逻辑取反在 truefalse 之间切换。
  • 事件通知:通过 bus.emitvisible 状态变化通知父组件。

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:销毁事件监听器是防止内存泄漏的关键一步,确保当组件被销毁时,不再响应事件总线上的事件。

完整代码实现

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 实现父子组件的高效通信,并在组件销毁时清理事件监听器,防止内存泄漏。此外,expandedvisible 状态的处理逻辑,使得组件能够灵活应对多层次的数据结构。

通过这些实现,你可以为项目中的树形数据展示提供强大的支持,同时保证组件的性能和可维护性。希望这篇文章为你构建复杂的递归组件提供思路和启发。

转载自:https://juejin.cn/post/7414733004885311523
评论
请登录