Vue中useSlots()获取VNode树丢失响应式问题求助
我正在开发一个管理默认插槽子节点布局的组件,预期使用方式如下:
<MyContainer> <FooComponent /> <BarComponent /> <BazComponent /> </MyContainer>
<MyContainer>负责设置Foo、Bar、Baz组件的位置与尺寸。
我提取容器中第一个节点(<FooComponent />)的示例代码如下:
<script setup lang="ts"> import { computed, useSlots } from 'vue'; const slots = useSlots() const defaultSlot = computed(() => slots.default?.()) </script> <template> <div style="background-color: red;"> <component v-if="defaultSlot" :is="defaultSlot[0]"></component> </div> </template>
问题:子组件虽能正常显示,但丢失了响应式——当其依赖的值变化时无法同步更新。我尝试过用闭包:is="() => defaultSlot[0]",看似恢复了响应式,但会导致父容器任意响应式属性变化时,子组件频繁重载,这个方案不可行。请问访问插槽的方式是否有误?或有其他方法能让子组件正常响应变更?
解决方案
问题出在你直接缓存了slots.default()的返回值。slots.default()每次调用都会生成新的VNode数组,且插槽内容的响应式依赖仅在调用该函数时才会被追踪。用computed缓存一次调用结果后,后续响应式变更不会触发重新调用slots.default(),导致子组件无法更新。
方法1:模板中直接访问插槽
不需要缓存插槽结果,在模板里直接调用插槽函数获取最新VNode:
<script setup lang="ts"> import { useSlots } from 'vue'; const slots = useSlots() </script> <template> <div style="background-color: red;"> <template v-if="slots.default"> <component :is="slots.default()[0]"></component> </template> </div> </template>
每次组件渲染都会重新调用slots.default(),能正确追踪插槽内部的响应式依赖,子组件会正常更新。
方法2:使用渲染函数(适合复杂逻辑)
如果需要在脚本中处理插槽内容,推荐用渲染函数确保响应式依赖被正确追踪:
<script setup lang="ts"> import { useSlots, h } from 'vue'; const slots = useSlots() const renderContainer = () => { const defaultSlot = slots.default?.() return h('div', { style: { backgroundColor: 'red' } }, defaultSlot?.[0]) } </script> <template> <renderContainer /> </template>
渲染函数每次执行都会重新调用slots.default(),既保证响应式更新,又能通过Vue的diff算法避免不必要的组件重载。
为什么闭包方案不可行?
用:is="() => defaultSlot[0]"时,父组件每次渲染都会执行该函数,生成全新的VNode,Vue会判定组件需要重新挂载,因此导致频繁重载。而直接调用slots.default()获取的VNode,Vue会进行diff比对,仅更新变化部分,不会完全重建组件。
内容的提问来源于stack exchange,提问作者Pop Flamingo

