封装 Tab 组件,在组件中使用插槽时遇到的问题
最近由于公司项目使用 vue3
和 vant
重构,并且所有的页面都需要按照设计图来,在使用 vant
组件库的 tabs
组件时,我发现组件的样式和设计图完全不一样,并且如果要修改样式,只能使用 :deep()
穿透来直接修改组件的样式,考虑到 tabs
组件我这边使用起来很简单,只需要切换即可,所以决定自己写一个简单的,以下是 tabs
组件的代码:
tabs.vue
<template> <div> <render-tab-bar/> <render-content/> </div> </template> <script setup> import {ref, useSlots, h} from 'vue' const globalProps = defineProps({ name: String, default: Number }) const slots = useSlots() let currentTab = ref(globalProps.default || 0) const emit = defineEmits(['update:default']) const dealClick = (tab) => { emit('update:default', tab) currentTab.value = tab } const renderOneButton = (name, tab, index) => h( 'label', { class: { 'tab-bar-button-item': true, 'tab-bar-button-item-active': currentTab.value === tab } }, [ h( 'input', { style: { display: 'none' }, type: 'radio', name: globalProps.name, value: name, onclick: () => dealClick(tab) }, {} ), name ] ) const renderTabBar = () => h( 'div', { class: "tab-bar-button-list" }, slots.default && slots.default().map((item, index) => { return renderOneButton(item.props?.name, item.props?.tab, index) }) ) const renderContent = () => { return ( slots.default && slots.default().find((item) => { if (currentTab.value === 0) { return true } return item.props?.tab === currentTab.value }) ) } </script> <style scoped> :deep(.tab-bar-button-list){ font-size: 16px; display: flex; flex-wrap: nowrap; justify-content: space-around; align-items: center; padding: 5px 0; margin: 10px 20px; border-radius: 90px; background-color: rgb(245, 247, 251); overflow-y: hidden; overflow-x: auto; .tab-bar-button-item { flex: 1; padding: 3px 0; text-align: center; border-radius: 40px; } .tab-bar-button-item-active { background: #FB4624; color: white; transition: all 0.3s; } } </style>
tab.vue
<template> <div> <slot></slot> </div> </template>
代码很简单,tabs
组件接收两个props
属性,name
是 input
单选框的属性,default
代表当前选中的是哪一个子tab
,tab
也接受两个props
属性,name
是input
的 value
值,也就是要在顶部显示的文字,tab
属性是标记当前tab
组件的唯一标识,用来和 tabs
组件的 default
属性判断是否相同,相同则添加被选中的样式。
index.vue
中使用
<template> <div class='content'> <Tabs :default=1> <tab name='第一页' :tab=1> 测试1 </tab> <tab name='第二页' :tab=2> 测试2 </tab> </Tabs> </div> </template> <script setup> import Tabs from '@/components/Tabs/tabs.vue' import Tab from '@/components/Tabs/tab.vue' </script> <style scoped> .content{ font-size: 20px; } </style>
可以看到,实现了正常的切换,另外,tab
插槽内使用组件也可以正常生效。
demo-child.vue
<script setup> defineProps({ msg: String }) </script> <template> <div>{{msg}}</div> </template> <style scoped> </style>
index.vue
中使用
<template> <div class='content'> <Tabs :default=1> <tab name='第一页' :tab=1 > <DemoChild msg='组件测试1'></DemoChild> </tab> <tab name='第二页' :tab=2> <DemoChild msg='组件测试2'></DemoChild> </tab> </Tabs> </div> </template> <script setup> import Tabs from '@/components/Tabs/tabs.vue' import Tab from '@/components/Tabs/tab.vue' import DemoChild from './demo-child.vue' </script> <style scoped> .content{ font-size: 20px; } </style>
但是,当我在demo-child
组件中使用插槽,将另一个组件放在 demo-child
的插槽内时,切换就失效了。
demo-child.vue
稍作修改
<script setup> defineProps({ msg: String }) </script> <template> <div>{{msg}}</div> <div style='color:red;'> <slot name='top'></slot> </div> <div style='color:green;'> <slot name='bottom'></slot> </div> </template> <style scoped> </style>
demo-Grandson.vue
<script setup> defineProps({ GrandMsg: String }) </script> <template> <div>{{GrandMsg}}</div> </template> <style scoped> </style>
index.vue
中使用
<template> <div class='content'> <Tabs :default=1> <tab name='第一页' :tab=1> <DemoChild msg='测试1'> <template #top> <DemoGrandson GrandMsg='测试1的插槽放的组件'></DemoGrandson> </template> </DemoChild> </tab> <tab name='第二页' :tab=2> <DemoChild msg='测试2'> <template #bottom> <DemoGrandson GrandMsg='测试2的插槽放的组件'></DemoGrandson> </template> </DemoChild> </tab> </Tabs> </div> </template> <script setup> import Tabs from '@/components/Tabs/tabs.vue' import Tab from '@/components/Tabs/tab.vue' import DemoChild from './demo-child.vue' import DemoGrandson from "./demo-grandson.vue" </script> <style scoped> .content{ font-size: 20px; } </style>
这是为什么呢,观察切换时的 dom
结构即可发现,实际上切换时,只有一个 div
发生了改变,也就是说,实际上组件并没有进行销毁和重新挂载,而是进行了复用,这是因为 Vue 3
使用虚拟DOM来优化dom
的更新。当组件的数据变化时,Vue
会先更新虚拟 dom
树,然后通过 diff
算法找出最小必要变更,并应用这些变更到真实 dom
上。Vue
会尽量复用现有的组件实例和 dom
元素,以提高性能。如果两个组件具有相同的类型和相似的 props/state
,Vue
可能会复用这些组件的实例,而不是销毁旧实例并创建新实例。解决方式也很简单,只要给组件一个 key
值,使组件更准确地识别哪些元素应该被保留、复用、移动或删除即可。
修改后的代码:
index.vue
<template> <div class='content'> <Tabs :default=1> <tab name='第一页' :tab=1> <DemoChild msg='测试1' :key='1'> <template #top> <DemoGrandson :key='1' GrandMsg='测试1的插槽放的组件'></DemoGrandson> </template> </DemoChild> </tab> <tab name='第二页' :tab=2> <DemoChild msg='测试2' :key='2'> <template #bottom> <DemoGrandson :key='2' GrandMsg='测试2的插槽放的组件'></DemoGrandson> </template> </DemoChild> </tab> </Tabs> </div> </template> <script setup> import Tabs from '@/components/Tabs/tabs.vue' import Tab from '@/components/Tabs/tab.vue' import DemoChild from './demo-child.vue' import DemoGrandson from "./demo-grandson.vue" </script> <style scoped> .content{ font-size: 20px; } </style>