Vue3中如何获取递归Group组件的首尾引用?
递归Group组件获取首尾嵌套Group引用的解决方案
我有一个支持递归的Group组件,内部包含ButtonsContainer组件(该组件提供聚焦首尾按钮的方法)。需要获取包含嵌套层级在内的第一个和最后一个Group组件的引用,但尝试递归函数时出现栈溢出问题,求实现方案。
原组件代码:
<script setup lang="ts"> const props = defineProps<{ group: Group }>(); const buttonsContainerRef = ref<InstanceType<typeof ButtonsContainer> | null>(null); const firstGroupRef = ref<InstanceType<typeof Group> | null>(null); const lastNestedGroupRef = ref<InstanceType<typeof Group> | null>(null); function getLastButtonsContainerRef(): Ref<InstanceType<typeof ButtonsContainer> | null> | undefined { if (props.group.groups.length > 0) { // return getLastButtonsContainerRef(); // still does not work:( getLastButtonsContainerRef(); } else { return buttonsContainerRef; } } </script> <template> <ul> <li v-if="group.buttons.length > 0 && !props.group.collapsed"> <ButtonsContainer :buttons="group.buttons" ref="buttonsContainerRef" /> </li> <li v-if="group.groups.length > 0 && !props.group.collapsed" v-for="nestedGroup in group.groups" :key="nestedGroup.uuid" > <Group :group="nestedGroup" class="nested-group" /> </li> </ul> </template>
问题分析
你之前的getLastButtonsContainerRef函数属于无限递归——它直接调用自身,没有处理子Group组件的引用传递,完全没触达嵌套的子组件,自然会触发栈溢出。要解决这个问题,需要基于数据结构或组件通信,精准收集目标Group的引用,而非盲目递归。
方案一:基于数据结构匹配(推荐,无组件通信依赖)
利用Group数据的递归结构,先遍历找到首尾叶子Group(无嵌套子Group或子Group折叠的节点),再在渲染时给对应Group组件绑定ref。
代码实现
<script setup lang="ts"> import { ref, computed } from 'vue'; import ButtonsContainer from './ButtonsContainer.vue'; const props = defineProps<{ group: Group }>(); const buttonsContainerRef = ref<InstanceType<typeof ButtonsContainer> | null>(null); const firstLeafGroupRef = ref<InstanceType<typeof Group> | null>(null); const lastLeafGroupRef = ref<InstanceType<typeof Group> | null>(null); // 递归查找第一个未折叠的叶子Group const findFirstLeafGroup = (group: Group): Group | null => { // 当前Group未折叠且无子Group,就是叶子节点 if (!group.collapsed && group.groups.length === 0) { return group; } // 遍历子Group,找第一个未折叠的分支 for (const nestedGroup of group.groups) { if (!nestedGroup.collapsed) { const result = findFirstLeafGroup(nestedGroup); if (result) return result; } } return null; }; // 递归查找最后一个未折叠的叶子Group const findLastLeafGroup = (group: Group): Group | null => { if (!group.collapsed && group.groups.length === 0) { return group; } // 从后往前遍历子Group,找最后一个未折叠的分支 for (let i = group.groups.length - 1; i >= 0; i--) { const nestedGroup = group.groups[i]; if (!nestedGroup.collapsed) { const result = findLastLeafGroup(nestedGroup); if (result) return result; } } return null; }; // 计算首尾叶子Group的uuid const firstLeafGroup = computed(() => findFirstLeafGroup(props.group)); const lastLeafGroup = computed(() => findLastLeafGroup(props.group)); </script> <template> <ul> <li v-if="group.buttons.length > 0 && !group.collapsed"> <ButtonsContainer :buttons="group.buttons" ref="buttonsContainerRef" /> </li> <li v-if="group.groups.length > 0 && !group.collapsed" v-for="nestedGroup in group.groups" :key="nestedGroup.uuid" > <Group :group="nestedGroup" class="nested-group" <!-- 匹配到首尾叶子Group时绑定对应ref --> :ref="nestedGroup.uuid === firstLeafGroup?.uuid ? el => firstLeafGroupRef = el : null" :ref="nestedGroup.uuid === lastLeafGroup?.uuid ? el => lastLeafGroupRef = el : null" /> </li> </ul> </template>
说明
- 先通过递归遍历数据结构,精准定位到需要获取引用的首尾Group节点
- 渲染时通过uuid匹配,给对应Group组件绑定ref,直接拿到组件实例
- 自动处理折叠状态,只收集未折叠的Group引用
方案二:通过Provide/Inject收集所有Group引用
如果需要获取所有嵌套Group的引用,再从中取首尾,可以用组件通信的方式收集:
代码实现
<script setup lang="ts"> import { ref, provide, inject, onMounted, onUnmounted, defineExpose, watch } from 'vue'; import ButtonsContainer from './ButtonsContainer.vue'; const props = defineProps<{ group: Group }>(); // 当前Group组件的实例引用 const selfRef = ref<InstanceType<typeof Group> | null>(null); const buttonsContainerRef = ref<InstanceType<typeof ButtonsContainer> | null>(null); // 定义收集/移除引用的类型 type GroupInstance = InstanceType<typeof Group>; type CollectFn = (instance: GroupInstance) => void; type RemoveFn = (instance: GroupInstance) => void; // 注入根组件提供的收集方法 const collectGroup = inject<CollectFn>('collectGroup'); const removeGroup = inject<RemoveFn>('removeGroup'); // 挂载时上报自身引用,卸载时移除 onMounted(() => { if (collectGroup && selfRef.value) { collectGroup(selfRef.value); } }); onUnmounted(() => { if (removeGroup && selfRef.value) { removeGroup(selfRef.value); } }); // 根Group组件(最外层)提供收集逻辑 if (!collectGroup) { const allGroupRefs = ref<GroupInstance[]>([]); // 提供收集方法 provide('collectGroup', (instance: GroupInstance) => { allGroupRefs.value.push(instance); }); // 提供移除方法 provide('removeGroup', (instance: GroupInstance) => { const index = allGroupRefs.value.indexOf(instance); if (index !== -1) allGroupRefs.value.splice(index, 1); }); // 直接获取首尾引用 const firstGroupRef = ref<GroupInstance | null>(null); const lastGroupRef = ref<GroupInstance | null>(null); // 监听数组变化更新首尾引用 watch(allGroupRefs, () => { firstGroupRef.value = allGroupRefs.value[0] || null; lastGroupRef.value = allGroupRefs.value[allGroupRefs.value.length - 1] || null; }, { immediate: true }); } // 暴露自身引用给父组件(必须,否则父组件无法通过ref获取实例) defineExpose({ buttonsContainerRef, // 可按需暴露其他方法/属性 }); </script> <template> <ul> <li v-if="group.buttons.length > 0 && !group.collapsed"> <ButtonsContainer :buttons="group.buttons" ref="buttonsContainerRef" /> </li> <li v-if="group.groups.length > 0 && !group.collapsed" v-for="nestedGroup in group.groups" :key="nestedGroup.uuid" > <Group :group="nestedGroup" class="nested-group" ref="selfRef" /> </li> </ul> </template>
说明
- 根Group组件提供收集和移除引用的方法,所有子Group组件注入后上报自身实例
- 根组件维护所有Group实例的数组,直接取数组首尾就是目标引用
- 组件卸载时自动移除引用,避免内存泄漏
内容的提问来源于stack exchange,提问作者David Gheeorghe
相关产品推荐
相关产品推荐

