You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 16:57:10