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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 19:11:30