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