Vue 3如何获取插槽中渲染的VueComponent实例?
Vue 3 Tabs组件实现问题:插槽实例获取与激活状态同步
需求背景
开发Tabs组件,要求父组件Tabs通过子组件TabContent的label插槽渲染标签栏,点击标签切换对应TabContent的显示状态。
Vue 2.x 实现方式
Tabs.vue
<template> <section class="tabs"> <ul class="tabs-labels"> <li v-for="tab in tabs" :key="tab._uid" :class="[{active: tab.isActive}, 'tab-label']" @click="selectTab(tab);" > {{ tab.$slots.label }} </li> </ul> <div class="tabs-content"> <slot/> </div> </section> </template> <script> export default { name: 'Tabs', data () { return { tabs: [], }; }, mounted () { this.tabs = this.$children.filter(tab => tab.$options.name === 'TabContent'); }, methods: { selectTab (selectedTab) { this.tabs.forEach(tab => { tab.isActive = (tab._uid === selectedTab._uid); }); }, }, }; </script>
TabContent.vue
<template> <div v-show="isActive" class="single-tab-content"> <slot/> </div> </template> <script> export default { name: 'TabContent', data () { return { isActive: false }; }, }; </script>
Vue 2中通过$children获取TabContent组件实例,直接修改实例的isActive属性实现状态切换。
Vue 3.x 遇到的问题
Vue 3移除了$children API,改为从$slots.default()获取插槽vNode:
this.tabs = this.$slots.default().filter(tab => tab.type.name === 'TabContent');
但获取到的是vNode而非组件实例,修改vNode上的isActive属性无法同步到实际渲染的组件,导致v-show状态不更新。
解决方案
方案一:状态提升(推荐,符合Vue 3响应式理念)
将激活状态提升到Tabs组件管理,通过props传递给TabContent,实现单向数据流。
修改后的Tabs.vue
<template> <section class="tabs"> <ul class="tabs-labels"> <li v-for="tab in tabList" :key="tab.key" :class="[{active: activeKey === tab.key}, 'tab-label']" @click="activeKey = tab.key" > <slot :name="`label-${tab.key}`" /> </li> </ul> <div class="tabs-content"> <slot :activeKey="activeKey" /> </div> </section> </template> <script setup> import { ref, computed, useSlots, watch } from 'vue' const activeKey = ref('tab1') const slots = useSlots() // 从插槽vNode提取tabKey,或由父组件显式传递 const tabList = computed(() => { return slots.default().map((vnode, idx) => ({ key: vnode.props?.tabKey || `tab${idx + 1}`, vnode })) }) // 支持v-model绑定 const props = defineProps({ modelValue: String }) const emit = defineEmits(['update:modelValue']) watch(activeKey, val => emit('update:modelValue', val)) </script>
修改后的TabContent.vue
<template> <div v-show="isActive" class="single-tab-content"> <slot /> </div> </template> <script setup> import { computed } from 'vue' const props = defineProps({ tabKey: String, activeKey: String }) const isActive = computed(() => props.tabKey === props.activeKey) </script>
父组件使用示例
<Tabs v-model="activeTab"> <TabContent tabKey="tab1"> <template #label-tab1>标签1</template> 内容1 </TabContent> <TabContent tabKey="tab2"> <template #label-tab2>标签2</template> 内容2 </TabContent> </Tabs>
方案二:获取组件实例(不推荐,耦合性高)
通过ref收集TabContent实例,直接操作组件状态:
修改后的Tabs.vue
<template> <section class="tabs"> <ul class="tabs-labels"> <li v-for="(tab, idx) in tabs" :key="tab._uid" :class="[{active: tab.isActive}, 'tab-label']" @click="selectTab(tab)" > <component :is="tab.$slots.label" /> </li> </ul> <div class="tabs-content"> <slot ref="tabRefs" /> </div> </section> </template> <script setup> import { ref, onMounted } from 'vue' const tabRefs = ref([]) const tabs = ref([]) onMounted(() => { tabs.value = tabRefs.value.filter(el => el.$options.name === 'TabContent') // 默认激活第一个标签 if (tabs.value.length) tabs.value[0].isActive = true }) const selectTab = (selectedTab) => { tabs.value.forEach(tab => { tab.isActive = tab._uid === selectedTab._uid }) } </script>
注意:此方式直接操作子组件内部状态,违背单向数据流原则,仅适用于特殊场景。
内容的提问来源于stack exchange,提问作者ArtemD
相关产品推荐
相关产品推荐

