这种模式通常用于处理树形结构数据(如菜单、评论、文件目录等)。Vue3 支持组件在其模板中直接引用自身,但需要注意递归的终止条件,以避免无限递归。
一、组件自身渲染自身的实现
1. 基本实现
-
在组件模板中,直接使用组件自身的标签名。
-
通过
name
属性定义组件名称,以便在模板中引用。
vue
复制
<template> <div> <p>{{ data.name }}</p> <ul v-if="data.children"> <li v-for="child in data.children" :key="child.id"> <!-- 递归渲染自身 --> <RecursiveComponent :data="child" /> </li> </ul> </div> </template> <script> export default { name: 'RecursiveComponent', // 定义组件名称 props: { data: { type: Object, required: true, }, }, }; </script>
2. 使用场景
-
树形菜单:渲染嵌套的菜单项。
-
评论系统:渲染嵌套的评论和回复。
-
文件目录:渲染嵌套的文件夹和文件。
二、递归组件的关键点
1. 终止条件
-
必须确保递归有终止条件,否则会导致无限递归和栈溢出。
-
在上面的例子中,
v-if="data.children"
是一个终止条件,当data.children
为空时,递归停止。
2. 组件名称
-
组件必须通过
name
属性定义名称,才能在模板中引用自身。 -
如果使用
<script setup>
语法,可以通过文件名隐式定义组件名称。
3. 数据格式
-
递归组件通常需要处理嵌套结构的数据,例如:
json
复制
{ "id": 1, "name": "Root", "children": [ { "id": 2, "name": "Child 1", "children": [] }, { "id": 3, "name": "Child 2", "children": [ { "id": 4, "name": "Grandchild 1", "children": [] } ] } ] }
三、完整示例
1. 递归组件
vue
复制
<template> <div> <p>{{ data.name }}</p> <ul v-if="data.children && data.children.length"> <li v-for="child in data.children" :key="child.id"> <RecursiveComponent :data="child" /> </li> </ul> </div> </template> <script> export default { name: 'RecursiveComponent', // 定义组件名称 props: { data: { type: Object, required: true, }, }, }; </script>
2. 使用递归组件
vue
复制
<template> <div> <h1>Tree Structure</h1> <RecursiveComponent :data="treeData" /> </div> </template> <script> import RecursiveComponent from './RecursiveComponent.vue'; export default { components: { RecursiveComponent, }, data() { return { treeData: { id: 1, name: 'Root', children: [ { id: 2, name: 'Child 1', children: [], }, { id: 3, name: 'Child 2', children: [ { id: 4, name: 'Grandchild 1', children: [], }, ], }, ], }, }; }, }; </script>
四、使用 <script setup>
语法
如果你使用 Vue3 的 <script setup>
语法,可以更简洁地实现递归组件:
1. 递归组件
vue
复制
<template> <div> <p>{{ data.name }}</p> <ul v-if="data.children && data.children.length"> <li v-for="child in data.children" :key="child.id"> <RecursiveComponent :data="child" /> </li> </ul> </div> </template> <script setup> defineProps({ data: { type: Object, required: true, }, }); </script>
2. 使用递归组件
vue
复制
<template> <div> <h1>Tree Structure</h1> <RecursiveComponent :data="treeData" /> </div> </template> <script setup> import RecursiveComponent from './RecursiveComponent.vue'; const treeData = { id: 1, name: 'Root', children: [ { id: 2, name: 'Child 1', children: [], }, { id: 3, name: 'Child 2', children: [ { id: 4, name: 'Grandchild 1', children: [], }, ], }, ], }; </script>
五、注意事项
-
性能问题:
-
递归组件可能会导致性能问题,尤其是在数据嵌套层级很深时。
-
可以通过虚拟滚动或懒加载优化性能。
-
-
终止条件:
-
必须确保递归有终止条件,否则会导致无限递归。
-
-
组件名称:
-
在模板中引用自身时,组件必须定义
name
属性。
-
-
数据格式:
-
确保数据结构正确,避免因数据问题导致渲染错误。
-
六、总结
-
Vue3 支持组件通过递归方式渲染自身。
-
递归组件适用于树形结构数据的渲染。
-
需要定义终止条件,避免无限递归。
-
可以使用
<script setup>
语法简化代码。
通过递归组件,可以轻松实现复杂的嵌套结构渲染!
如下是容器和折叠面板,拖拽组件,自动拖拽布局的代码示例:
<template>
<Col v-bind="colPropsComputed" v-if="['grid', 'collapse'].includes(item.fieldTypeInnerKey)">
<div
:class="['box', activeId === (item.fieldId || item.id) ? 'active-form-item' : '']"
:style="{ width: item.fieldWidth ?? '50%' }"
@click.stop="emits('activeItem', item)"
>
<Row class="grid-row" v-bind="item.componentProps" v-if="'grid' === item.fieldTypeInnerKey">
<Col
class="grid-col"
v-for="(colItem, index) in item.children"
:key="index"
:span="Number(colItem.fieldWidth)"
>
<draggable
class="drawing-board draggable-box"
:component-data="{ name: 'list', tag: 'div', type: 'transition-group' }"
v-bind="{
group: 'componentsGroup',
animation: 340,
}"
item-key="fieldName"
v-model="colItem.children"
>
<template #item="{ element }">
<LayoutItem
:drawing-list="drawingList"
:item="element"
:active-id="activeId"
:form-rule="formRule"
:inner-key="innerKey"
:isDisabled="isDisabled"
v-bind="$attrs"
@delete-item="emits('deleteItem', $event)"
@delete-grid="emits('deleteGrid', $event)"
@active-item="emits('activeItem', $event)"
@change-item="(key, val) => emits('changeItem', key, val)"
@is-required="(id, k) => emits('isRequired', id, k)"
@is-read="(id, k) => emits('isRead', id, k)"
/>
</template>
</draggable>
</Col>
</Row>
<Collapse default-active-key="1" v-if="'collapse' === item.fieldTypeInnerKey">
<CollapsePanel key="1" :header="item.displayName">
<div :class="['collapse-box']" :style="{ width: item.fieldWidth ?? '50%' }">
<draggable
class="drawing-board draggable-box"
:component-data="{ name: 'list', tag: 'div', type: 'transition-group' }"
v-bind="{
group: 'componentsGroup',
animation: 340,
}"
item-key="fieldName"
v-model="item.children"
>
<template #item="{ element }">
<LayoutItem
:drawing-list="drawingList"
:item="element"
:active-id="activeId"
:form-rule="formRule"
:inner-key="innerKey"
:isDisabled="isDisabled"
v-bind="$attrs"
@delete-item="emits('deleteItem', $event)"
@delete-grid="emits('deleteGrid', $event)"
@active-item="emits('activeItem', $event)"
@change-item="(key, val) => emits('changeItem', key, val)"
@is-required="(id, k) => emits('isRequired', id, k)"
@is-read="(id, k) => emits('isRead', id, k)"
/>
</template>
</draggable>
</div>
</CollapsePanel>
</Collapse>
<Operate :isDisabled="isDisabled" :item="item" @delete-item="emits('deleteGrid', item)" />
</div>
</Col>
<draggable-item
v-else
v-bind="$attrs"
:drawing-list="drawingList"
:item="item"
:active-id="activeId"
:form-rule="formRule"
:inner-key="innerKey"
:isDisabled="isDisabled"
@delete-item="emits('deleteItem', $event)"
@active-item="emits('activeItem', $event)"
@change-item="(key, val) => emits('changeItem', key, val)"
@is-required="(id, k) => emits('isRequired', id, k)"
@is-read="(id, k) => emits('isRead', id, k)"
/>
</template>
<script lang="ts" setup name="LayoutItem">
import { computed, defineComponent, PropType, reactive, toRefs } from 'vue';
import draggable from 'vuedraggable';
import DraggableItem from './DraggableItem.vue';
import Operate from './Operate.vue';
import { Row, Col, Collapse, CollapsePanel } from 'ant-design-vue';
const props = defineProps({
item: {
type: Object as PropType<any>,
default: () => ({}),
},
formRule: {
type: Object as PropType<any>,
default: () => ({}),
},
drawingList: {
type: Object as PropType<any>,
default: () => ({}),
},
activeId: {
type: String as PropType<string>,
default: '',
},
innerKey: {
type: String as PropType<string>,
default: '',
},
isDisabled: Boolean,
});
const emits = defineEmits([
'deleteGrid',
'dragStart',
'handleColAdd',
'handle-copy',
'handle-delete',
'deleteItem',
'activeItem',
'changeItem',
'isRequired',
'isRead',
]);
const colPropsComputed = computed(() => {
const { colProps = { span: 24 } } = props.item;
return colProps;
});
</script>