在vue2和vue3中展示实现可折叠树形菜单组件的代码示例,分析树形结构展示和折叠功能的实现逻辑

大白话在vue2和vue3中展示实现可折叠树形菜单组件的代码示例,分析树形结构展示和折叠功能的实现逻辑

前端小伙伴们,有没有被“树形菜单”搞到挠头过?做权限管理要展示部门层级,做文件管理器要渲染目录结构,点父节点展开子节点,点子节点还要触发操作……今天咱们用大白话聊Vue2和Vue3里的可折叠树形菜单,从递归渲染到折叠逻辑,手把手教你写出能打能抗的组件!

一、树形菜单的"开发痛点"

先说说我最近接的项目:给企业做后台管理系统,需要展示组织架构树——总公司下有分公司,分公司下有部门,部门下还有小组。需求是:

  • 点击父节点能折叠/展开子节点;
  • 点击叶子节点能选中并显示详情;
  • 支持动态增删节点(比如新成立一个部门)。

结果开发时遇到这些坑:

  1. 递归渲染不会写:节点套节点,不知道怎么用组件循环自己;
  2. 折叠状态乱套:点父节点,子节点没反应,或者所有节点一起展开;
  3. 动态更新不生效:新增一个子节点,页面死活不显示,得刷新才出来。

二、树形菜单的"两个核心"

要搞定树形菜单,得先明白它的底层逻辑——递归渲染+状态管理。就像俄罗斯套娃,每个套娃里可能还有小套娃,我们需要:

  1. 递归组件:用一个组件循环调用自己,处理无限层级;
  2. 状态记录:每个节点存一个isOpen状态,控制折叠/展开。

1. 递归组件:自己调自己的"套娃组件"

树形结构的特点是每个节点可能有子节点,形成层级关系。Vue的递归组件允许组件在模板中调用自己,只要:

  • 组件定义name属性(递归时用这个名字);
  • 子节点通过v-ifv-show控制显示(折叠时隐藏)。

2. 状态管理:每个节点的"折叠开关"

每个节点需要一个isOpen(布尔值)记录是否展开:

  • isOpen: true → 展开,显示子节点;
  • isOpen: false → 折叠,隐藏子节点。

Vue2和Vue3的响应式原理不同:

  • Vue2:用Object.defineProperty劫持属性,修改对象/数组时需用this.$set触发响应式;
  • Vue3:用Proxy代理整个对象,直接修改属性即可触发更新(更省心)。

三、代码示例:Vue2和Vue3的实现对比

示例1:Vue2版本(选项式API)

先看Vue2怎么实现。核心是递归组件+$emit传递事件,用this.$set更新响应式状态。

步骤1:准备树形数据(父节点+子节点)
// 模拟树形数据(实际可能从接口获取)
const treeData = [
  {
    id: 1,
    label: '总公司',
    isOpen: false, // 初始折叠
    children: [
      {
        id: 11,
        label: '北京分公司',
        isOpen: false,
        children: [{ id: 111, label: '技术部' }, { id: 112, label: '财务部' }]
      },
      {
        id: 12,
        label: '上海分公司',
        isOpen: false,
        children: [{ id: 121, label: '市场部' }]
      }
    ]
  }
];
步骤2:编写递归组件(TreeItem.vue)
<template>
  <!-- 每个节点的容器 -->
  <div class="tree-node">
    <!-- 点击区域:图标+节点名称 -->
    <div 
      class="node-header" 
      @click="toggle" 
      :class="{ 'is-open': node.isOpen }"
    >
      <!-- 有子节点时显示折叠图标(三角),否则显示空 -->
      <span class="icon">{{ node.children?.length ? (node.isOpen ? '▼' : '▶') : '' }}</span>
      <span class="label">{{ node.label }}</span>
    </div>

    <!-- 子节点区域:isOpen为true时显示,递归渲染TreeItem -->
    <div class="children" v-if="node.children?.length && node.isOpen">
      <!-- 循环子节点,每个子节点用TreeItem组件渲染 -->
      <TreeItem 
        v-for="child in node.children" 
        :key="child.id" 
        :node="child" 
        @toggle-node="handleChildToggle"
      />
    </div>
  </div>
</template>

<script>
export default {
  name: 'TreeItem', // 关键:递归组件必须定义name
  props: {
    node: {
      type: Object,
      required: true
    }
  },
  methods: {
    // 点击时切换当前节点的isOpen状态
    toggle() {
      if (this.node.children?.length) { // 有子节点才能折叠
        this.$emit('toggle-node', this.node.id); // 通知父组件更新状态
      }
    },
    // 子节点触发折叠时,更新父组件状态(Vue2需用$set保证响应式)
    handleChildToggle(childId) {
      this.$emit('toggle-node', childId);
    }
  }
};
</script>

<style scoped>
.tree-node {
  padding-left: 16px; /* 层级缩进 */
}
.node-header {
  cursor: pointer;
  padding: 4px 0;
  display: flex;
  align-items: center;
  gap: 8px;
}
.icon {
  font-size: 12px;
  width: 14px;
}
.children {
  /* 子节点区域的过渡动画 */
  transition: all 0.3s;
}
.is-open .icon {
  transform: rotate(90deg); /* 展开时三角旋转 */
}
</style>
步骤3:父组件使用(Tree.vue)
<template>
  <div class="tree-container">
    <!-- 根节点循环渲染TreeItem -->
    <TreeItem 
      v-for="rootNode in treeData" 
      :key="rootNode.id" 
      :node="rootNode" 
      @toggle-node="toggleNode"
    />
  </div>
</template>

<script>
import TreeItem from './TreeItem.vue';
import { treeData } from './data.js';

export default {
  name: 'Tree',
  components: { TreeItem },
  data() {
    return {
      treeData: treeData
    };
  },
  methods: {
    // 处理节点折叠事件(Vue2用$set更新对象属性)
    toggleNode(nodeId) {
      // 递归查找目标节点并切换isOpen
      this.toggleNodeRecursive(this.treeData, nodeId);
    },
    toggleNodeRecursive(nodes, targetId) {
      for (const node of nodes) {
        if (node.id === targetId) {
          // Vue2中修改对象属性需用this.$set触发响应式
          this.$set(node, 'isOpen', !node.isOpen);
          return;
        }
        if (node.children) {
          this.toggleNodeRecursive(node.children, targetId);
        }
      }
    }
  }
};
</script>

示例2:Vue3版本(组合式API)

Vue3用组合式API和reactive/ref管理状态,响应式更灵活,不需要this.$set

步骤1:树形数据(和Vue2相同)
// 数据结构和Vue2完全一致
const treeData = [/* 和上面一样 */];
步骤2:递归组件(TreeItem.vue)
<template>
  <!-- 结构和Vue2类似,逻辑更简洁 -->
  <div class="tree-node">
    <div 
      class="node-header" 
      @click="toggle" 
      :class="{ 'is-open': node.isOpen }"
    >
      <span class="icon">{{ node.children?.length ? (node.isOpen ? '▼' : '▶') : '' }}</span>
      <span class="label">{{ node.label }}</span>
    </div>

    <div class="children" v-if="node.children?.length && node.isOpen">
      <!-- 递归渲染子节点 -->
      <TreeItem 
        v-for="child in node.children" 
        :key="child.id" 
        :node="child" 
        @toggle-node="handleChildToggle"
      />
    </div>
  </div>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue';

// 定义Props和事件
const props = defineProps({
  node: {
    type: Object,
    required: true
  }
});
const emits = defineEmits(['toggle-node']);

// 点击时触发折叠事件
const toggle = () => {
  if (props.node.children?.length) {
    emits('toggle-node', props.node.id);
  }
};

// 子节点事件传递
const handleChildToggle = (childId) => {
  emits('toggle-node', childId);
};
</script>

<style scoped>
/* 和Vue2的CSS完全一致 */
</style>
步骤3:父组件使用(Tree.vue)
<template>
  <div class="tree-container">
    <TreeItem 
      v-for="rootNode in treeData" 
      :key="rootNode.id" 
      :node="rootNode" 
      @toggle-node="toggleNode"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue';
import TreeItem from './TreeItem.vue';
import { treeData } from './data.js';

// 用ref管理树形数据(自动响应式)
const treeDataRef = ref([...treeData]);

// 处理折叠事件(Vue3直接修改对象属性即可触发更新)
const toggleNode = (targetId) => {
  // 递归查找并切换isOpen
  const toggleRecursive = (nodes) => {
    for (const node of nodes) {
      if (node.id === targetId) {
        node.isOpen = !node.isOpen; // Vue3的Proxy自动响应式
        return;
      }
      if (node.children) {
        toggleRecursive(node.children);
      }
    }
  };
  toggleRecursive(treeDataRef.value);
};
</script>

四、Vue2 vs Vue3实现差异

用表格对比两者的核心差异,帮你快速理解:

对比项Vue2(选项式API)Vue3(组合式API)
响应式更新需用this.$set修改对象属性直接修改属性(Proxy自动劫持)
组件APIdata/methods/propsdefineProps/defineEmits/setup
状态管理依赖this上下文更灵活(ref/reactive)
递归组件写法需定义name属性无需显式name(自动识别)
事件传递$emit+父组件$ondefineEmits+emits函数
动态节点更新可能漏用$set导致不更新直接修改即可(不易出错)

五、面试题回答方法

正常回答(结构化):

“实现Vue可折叠树形菜单的核心是递归组件+状态管理:

  1. 递归组件:通过组件自调用渲染无限层级,每个节点渲染时判断是否有子节点,有则递归渲染子组件;
  2. 状态管理:每个节点维护isOpen状态,点击时切换该状态;
  3. Vue2注意点:修改对象/数组属性时需用this.$set触发响应式;
  4. Vue3优势:使用Proxy实现响应式,直接修改属性即可更新视图;
  5. 事件传递:子节点通过$emit/emits通知父节点更新状态,父节点递归查找目标节点并修改isOpen。”

大白话回答(接地气):

“树形菜单就像套娃,每个套娃里可能还有小套娃。用Vue的递归组件,让每个节点自己调用自己渲染子节点。每个套娃有个‘开关’(isOpen),点击就打开或关闭。Vue2里改开关得用$set喊一声‘视图该更新啦’,Vue3直接改开关就行,视图自己会看。子套娃点击时,告诉父套娃‘我要变’,父套娃就顺着藤找到那个套娃,把开关状态改了~”

六、总结:3个核心步骤+2个避坑指南

3个核心步骤:

  1. 定义树形数据:每个节点包含idlabelchildrenisOpen
  2. 编写递归组件:用name(Vue2)或自动识别(Vue3)实现自调用;
  3. 管理折叠状态:点击时切换isOpen,Vue2用$set,Vue3直接修改。

2个避坑指南:

  • Vue2的$set:添加/修改对象属性时,必须用this.$set(obj, key, value),否则视图不更新;
  • 避免过度递归:如果树形层级极深(比如100层),可能导致性能问题,建议限制层级或用虚拟滚动;
  • 事件冒泡:点击子节点时,父节点可能被误触发,可用stop修饰符(@click.stop)阻止冒泡。

七、扩展思考:4个高频问题解答

问题1:如何默认展开某个节点?

解答:初始化时设置isOpen: true。比如:

const treeData = [
  {
    id: 1,
    label: '总公司',
    isOpen: true, // 默认展开
    children: [/* ... */]
  }
];

问题2:如何动态添加一个子节点?

解答:找到目标父节点,用push添加子节点(Vue3直接push,Vue2需用this.$setthis.$forceUpdate):

// Vue3示例:找到id=11的节点,添加子节点
const parentNode = findNode(treeDataRef.value, 11);
parentNode.children.push({ id: 113, label: '新部门', isOpen: false });

问题3:如何实现全选/反选?

解答:给每个节点添加isChecked状态,递归修改所有子节点的isChecked

// 递归全选函数
const checkAll = (node, checked) => {
  node.isChecked = checked;
  if (node.children) {
    node.children.forEach(child => checkAll(child, checked));
  }
};

问题4:如何优化性能?

解答

  • 虚拟滚动:层级过深时,只渲染可见区域的节点;
  • 缓存渲染:用keep-alive缓存已展开的节点;
  • 异步加载:子节点数据量大时,点击父节点再请求子数据(需添加isLoading状态)。

结尾:树形菜单的终极目标——“丝滑又听话”

可折叠树形菜单是前端最常用的组件之一,掌握它的递归渲染和状态管理,能让你的页面更灵活、用户体验更流畅。记住:递归组件是骨架,状态管理是灵魂,Vue2和Vue3的差异在响应式实现

下次遇到树形菜单需求,你可以拍着胸脯说:“这题我会,3步搞定!” 如果这篇文章帮你理清了思路,记得点个收藏,咱们下期不见不散!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

前端布洛芬

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

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

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

打赏作者

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

抵扣说明:

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

余额充值