封装 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>