一、先搞懂递归组件到底是什么
很多人第一次听到“递归组件”会觉得是个很玄的概念,其实说白了就是“自己调用自己的组件”。举个生活里的例子:你给朋友拍一张照片,照片里的你手里拿着这张照片,照片里的照片里又有你拿着照片……只要有个“别再继续拍”的规则,就不会无限循环下去。递归组件就是这个道理,核心要解决两个问题:一是组件怎么自己调用自己,二是怎么停止这个循环。
二、从简单场景练手:树形菜单的递归实现
先拿大家最熟悉的树形菜单来练手,树形菜单的特点是:一级菜单下面可能有二级,二级下面可能有三级,层级数不确定。要实现它,就得用递归组件。
2.1 先明确需求和数据结构
我们要做的树形菜单,点击父菜单能展开/折叠子菜单,子菜单的层级不限。首先得定义好菜单的数据结构,比如每个菜单项都有名称、是否展开、子菜单列表这几个属性。
2.2 核心实现思路
首先要做一个基础的菜单项组件,这个组件要能渲染自己的名称,还要能判断有没有子菜单:如果有子菜单,就调用自己来渲染子菜单;如果没有子菜单,就停止。这里的关键是“子菜单的渲染用父组件自己”,也就是组件自引用。
2.3 完整代码示例(技术栈:Vue 3)
首先是父组件,用来初始化数据和渲染根级菜单:
<template>
<!-- 渲染根级菜单,传入根菜单数据 -->
<MenuNode :menuData="rootMenu" />
</template>
<script setup>
// 导入我们写的递归菜单项组件
import MenuNode from './MenuNode.vue'
// 定义根菜单数据,层级不限
const rootMenu = [
{
name: '前端开发',
open: false, // 控制是否展开子菜单
children: [
{
name: 'Vue框架',
open: false,
children: [
{ name: 'Vue 3', open: false, children: [] },
{ name: 'Vue 2', open: false, children: [] }
]
},
{
name: 'React框架',
open: false,
children: [
{ name: 'React 18', open: false, children: [] },
{ name: 'React 17', open: false, children: [] }
]
}
]
},
{
name: '后端开发',
open: false,
children: [
{ name: 'Java', open: false, children: [] },
{ name: 'Python', open: false, children: [] }
]
}
]
</script>
然后是核心的递归组件MenuNode.vue,这个组件自己调用自己:
<template>
<div class="menu-node">
<!-- 渲染当前菜单项的名称,点击切换展开状态 -->
<div class="menu-title" @click="toggleOpen">
<!-- 如果有子菜单,显示展开/折叠的箭头 -->
<span v-if="menuData.children.length > 0">
{{ menuData.open ? '▼' : '▶' }}
</span>
{{ menuData.name }}
</div>
<!-- 只有当菜单展开且有子菜单时,才渲染子菜单 -->
<div class="menu-children" v-if="menuData.open && menuData.children.length > 0">
<!-- 这里是关键:用自己这个组件渲染子菜单,也就是自引用 -->
<MenuNode
v-for="child in menuData.children"
:key="child.name"
:menuData="child"
/>
</div>
</div>
</template>
<script setup>
// 接收父组件传过来的当前菜单数据
const props = defineProps({
menuData: {
type: Object,
required: true
}
})
// 切换菜单的展开状态
const toggleOpen = () => {
props.menuData.open = !props.menuData.open
}
</script>
<style scoped>
.menu-node {
margin-left: 20px; /* 每一层缩进,区分层级 */
}
.menu-title {
cursor: pointer;
padding: 4px 0;
}
.menu-children {
margin-top: 4px;
}
</style>
2.4 这里的关键细节
- 组件自引用:
MenuNode.vue里又引入了自己,这就是递归的核心,相当于子菜单用和父菜单一样的逻辑渲染。 - 终止条件:只有当
menuData.open为真且menuData.children.length > 0时,才会继续渲染子菜单。如果没有子菜单,或者菜单没展开,就停止递归,不会无限循环。
三、进阶场景:组织架构图的递归实现
树形菜单的逻辑搞懂了,组织架构图其实就是换了个样式的递归组件,核心逻辑完全一样——只不过组织架构图是展示层级关系,样式上可能用连线、圆角框来区分层级,而不是简单的文字和箭头。
3.1 组织架构图的核心逻辑
组织架构图的每个节点是一个人员/部门,每个节点下面可能有子部门/子人员,层级同样不确定。所以还是用递归组件:每个节点组件渲染自己的信息,然后调用自己渲染子节点。
3.2 组织架构图的代码示例(技术栈:Vue 3)
首先是父组件,定义组织架构数据:
<template>
<!-- 渲染根节点,这里是公司CEO -->
<OrgNode :orgData="ceo" />
</template>
<script setup>
import OrgNode from './OrgNode.vue'
// 组织架构数据,层级不限
const ceo = {
name: '张三',
position: 'CEO',
children: [
{
name: '李四',
position: '技术总监',
children: [
{
name: '王五',
position: '前端经理',
children: [
{ name: '赵六', position: '前端开发', children: [] },
{ name: '钱七', position: '前端开发', children: [] }
]
},
{
name: '孙八',
position: '后端经理',
children: [
{ name: '李九', position: '后端开发', children: [] }
]
}
]
},
{
name: '周十',
position: '产品总监',
children: [
{ name: '吴十一', position: '产品经理', children: [] }
]
}
]
}
</script>
然后是递归组件OrgNode.vue:
<template>
<div class="org-node">
<!-- 渲染当前节点的信息 -->
<div class="org-info">
<p class="name">{{ orgData.name }}</p>
<p class="position">{{ orgData.position }}</p>
</div>
<!-- 只有有子节点时,才渲染子节点区域 -->
<div class="org-children" v-if="orgData.children.length > 0">
<!-- 这里是连线,用来连接父节点和子节点 -->
<div class="line"></div>
<!-- 子节点区域,横向排列多个子节点 -->
<div class="children-container">
<!-- 用自己渲染子节点,自引用 -->
<OrgNode
v-for="child in orgData.children"
:key="child.name"
:orgData="child"
/>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
orgData: {
type: Object,
required: true
}
})
</script>
<style scoped>
.org-node {
text-align: center;
margin: 20px 0;
}
.org-info {
display: inline-block;
border: 1px solid #ccc;
border-radius: 8px;
padding: 10px 20px;
}
.name {
margin: 0;
font-weight: bold;
}
.position {
margin: 5px 0 0 0;
color: #666;
}
.org-children {
position: relative;
}
/* 父节点到子节点区域的竖线 */
.line {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
width: 1px;
height: 20px;
background: #ccc;
}
.children-container {
display: flex;
justify-content: center;
gap: 40px; /* 子节点之间的间距 */
margin-top: 20px;
}
</style>
3.3 这里的关键变化
组织架构图的递归逻辑和树形菜单完全一致,只是样式做了调整:用连线来体现层级关系,子节点横向排列。这也说明递归组件的核心是逻辑复用,样式可以灵活调整。
四、递归组件的核心细节:自引用和终止条件
递归组件的两个核心问题,一定要搞清楚,不然很容易出问题。
4.1 组件自引用的两种方式
组件自引用就是“在组件内部引入自己”,不同框架的实现方式略有不同:
- Vue 3:可以直接在组件内部引入自己,因为Vue的组件名默认就是文件名,所以只要文件名和组件名一致,就能直接引入。
- React:React的组件是函数,所以自引用的方式是在函数内部调用自己,比如:
const OrgNode = ({ orgData }) => { return ( <div> <div>{orgData.name}</div> {orgData.children.length > 0 && orgData.children.map(child => ( <OrgNode key={child.name} orgData={child} /> ))} </div> ) }
4.2 终止条件的必要性
终止条件是递归组件的“刹车”,如果没有终止条件,组件会无限渲染下去,直到浏览器内存溢出。终止条件的核心是“当没有可继续递归的内容时,停止渲染子组件”,常见的终止条件有:
- 没有子数据(比如
children.length === 0) - 父组件的状态限制(比如菜单没展开,就不渲染子菜单)
- 层级限制(比如最多渲染3层,超过3层就停止)
举个反例:如果树形菜单的终止条件写错了,比如把v-if="menuData.open && menuData.children.length > 0"写成v-if="menuData.children.length > 0",那么只要有子菜单,就会一直渲染,哪怕菜单没展开,这样不仅会浪费性能,还可能出现逻辑错误。
五、递归组件的应用场景、优缺点和注意事项
5.1 应用场景
除了树形菜单和组织架构图,递归组件还适合所有“层级不确定的树状结构”场景,比如:
- 文件目录树(比如电脑的文件夹结构)
- 评论区(评论下面有子评论,子评论下面还有子评论)
- 多级分类(比如电商的商品分类,一级分类下面有二级,二级下面有三级)
- 权限菜单(不同角色的菜单层级不同)
5.2 技术优缺点
优点
- 代码简洁:不用为不同层级写不同的组件,一套逻辑搞定所有层级。
- 扩展性强:不管层级增加多少,只要数据结构不变,组件不用改。
- 逻辑复用:所有层级的渲染逻辑完全一致,维护起来很方便。
缺点
- 性能问题:如果层级很深,或者每个层级的子节点很多,递归渲染会占用较多的内存和CPU,可能导致页面卡顿。
- 调试困难:递归组件的调用链是层层嵌套的,出问题的时候很难定位到具体是哪一层的逻辑错了。
- 容易出现死循环:如果终止条件没写对,会导致无限渲染,直接让页面崩溃。
5.3 注意事项
- 一定要写对终止条件:这是递归组件的核心,写完一定要测试,比如把层级拉到最深,看会不会停止。
- 控制层级深度:如果层级特别深(比如超过10层),可以考虑加一个最大层级限制,超过的层级用“展开更多”的方式加载,避免一次性渲染太多内容。
- 优化性能:如果子节点很多,可以用虚拟滚动(比如Vue的
vue-virtual-scroller)来优化,只渲染可见区域的节点,减少性能消耗。 - 加唯一标识:渲染子节点的时候一定要加唯一的
key,比如上面示例里的key="child.name",避免框架在更新的时候出错。
六、文章总结
递归组件本质上是“用一套逻辑处理不确定层级的树状结构”,核心是组件自引用和正确的终止条件。从树形菜单到组织架构图,核心逻辑完全一致,只是样式和业务场景不同。它适合所有层级不确定的树状结构场景,能大幅简化代码,但也有性能和调试的问题,需要注意优化。
评论
围绕“递归组件设计思路:从树形菜单到组织架构图,处理组件自引用与终止条件的细节,实现任意层级数据的渲染能力”参与讨论