Vue3 組件通信方式全解(10種方案)
一、組件通信方式概覽
通信方式 | 適用場景 | 數據流向 | 復雜度 |
---|---|---|---|
Props/自定義事件 | 父子組件簡單通信 | 父 ? 子 | ? |
v-model 雙向綁定 | 父子表單組件 | 父 ? 子 | ?? |
Provide/Inject | 跨層級組件通信 | 祖先 → 后代 | ?? |
事件總線 | 任意組件間通信 | 任意方向 | ??? |
模板引用(ref) | 父操作子組件 | 父 → 子 | ? |
Pinia 狀態管理 | 復雜應用狀態共享 | 全局共享 | ??? |
瀏覽器存儲 | 持久化數據共享 | 全局共享 | ??? |
attrs/attrs/listeners | 透傳屬性/事件 | 父 → 深層子組件 | ?? |
作用域插槽 | 子向父傳遞渲染內容 | 子 → 父 | ?? |
路由參數 | 頁面間數據傳遞 | 頁面間 | ? |
二、核心通信方案詳解
1. Props / 自定義事件(父子通信)
<!-- 父組件 Parent.vue -->
<template><Child :message="parentMsg" @update="handleUpdate"/>
</template><script setup>
import { ref } from 'vue'
const parentMsg = ref('Hello from parent')const handleUpdate = (newMsg) => {parentMsg.value = newMsg
}
</script><!-- 子組件 Child.vue -->
<script setup>
defineProps(['message'])
const emit = defineEmits(['update'])const sendToParent = () => {emit('update', 'New message from child')
}
</script>
2. v-model 雙向綁定(表單場景)
<!-- 父組件 -->
<CustomInput v-model="username" /><!-- 子組件 CustomInput.vue -->
<template><input :value="modelValue"@input="$emit('update:modelValue', $event.target.value)">
</template><script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
3. Provide/Inject(跨層級)
// 祖先組件
import { provide, ref } from 'vue'
const theme = ref('dark')provide('theme', {theme,toggleTheme: () => {theme.value = theme.value === 'dark' ? 'light' : 'dark'}
})// 后代組件
import { inject } from 'vue'
const { theme, toggleTheme } = inject('theme')
4. 事件總線 mitt(任意組件)
// eventBus.js
import mitt from 'mitt'
export const emitter = mitt()// 組件A(發送)
emitter.emit('global-event', { data: 123 })// 組件B(接收)
emitter.on('global-event', (data) => {console.log(data)
})
5. Pinia 狀態管理(復雜應用)
// stores/user.js
export const useUserStore = defineStore('user', {state: () => ({ name: 'Alice' }),actions: {updateName(newName) {this.name = newName}}
})// 組件使用
const userStore = useUserStore()
userStore.updateName('Bob')
6. 作用域插槽(子傳父)
<!-- 子組件 -->
<template><slot :data="childData" />
</template><script setup>
const childData = { message: 'From child' }
</script><!-- 父組件 -->
<Child><template #default="{ data }">{{ data.message }}</template>
</Child>
三、進階通信技巧
1. 多層屬性透傳
// 父組件
<GrandParent><Parent><Child /></Parent>
</GrandParent>// GrandParent.vue
<template><Parent v-bind="$attrs"><slot /></Parent>
</template>// 使用 inheritAttrs: false 關閉自動繼承
2. 動態組件通信
<component :is="currentComponent" @custom-event="handleEvent"
/>
3. 自定義 Hook 封裝
// hooks/useCounter.js
export function useCounter(initial = 0) {const count = ref(initial)const increment = () => count.value++return { count, increment }
}// 組件A
const { count: countA } = useCounter()// 組件B
const { count: countB } = useCounter(10)
4. 全局事件總線加強版
// eventBus.ts
type EventMap = {'user-login': { userId: string }'cart-update': CartItem[]
}export const emitter = mitt<EventMap>()// 安全使用
emitter.emit('user-login', { userId: '123' })
四、最佳實踐指南
-
簡單場景優先方案
-
父子通信:Props + 自定義事件
-
表單組件:v-model
-
簡單共享數據:Provide/Inject
-
-
復雜應用推薦方案
-
全局狀態管理:Pinia
-
跨組件通信:事件總線
-
持久化數據:localStorage + Pinia 插件
-
-
性能優化技巧
-
避免在 Props 中傳遞大型對象
-
使用 computed 屬性優化渲染
-
對頻繁觸發的事件進行防抖處理
-
及時清理事件監聽器
-
-
TypeScript 增強
// Props 類型定義
defineProps<{title: stringdata: number[]
}>()// 事件類型定義
defineEmits<{(e: 'update', value: string): void(e: 'delete', id: number): void
}>()
五、常見問題解決方案
Q1: 如何避免 Props 層層傳遞?
? 使用 Provide/Inject 或 Pinia
Q2: 子組件如何修改父組件數據?
? 通過自定義事件通知父組件修改
Q3: 如何實現兄弟組件通信?
? 方案1:通過共同的父組件中轉
? 方案2:使用事件總線或 Pinia
Q4: 如何保證響應式數據安全?
? 使用 readonly 限制修改權限:
provide('config', readonly(config))
Q5: 如何實現跨路由組件通信?
? 使用 Pinia 狀態管理
? 通過路由參數傳遞
? 使用 localStorage 持久化存儲
? ?
插槽詳解
一、插槽核心概念
1. 插槽作用
允許父組件向子組件插入自定義內容,實現組件的高度可定制化
2. 組件通信對比
通信方式 | 數據流向 | 內容類型 |
---|---|---|
Props | 父 → 子 | 純數據 |
插槽 | 父 → 子 | 模板/組件/HTML片段 |
二、基礎插槽類型
1. 默認插槽
子組件:
<!-- ChildComponent.vue -->
<template><div class="card"><slot>默認內容(父組件未提供時顯示)</slot></div>
</template>
父組件:
<ChildComponent><p>自定義卡片內容</p>
</ChildComponent>
2. 具名插槽
子組件:
<template><div class="layout"><header><slot name="header"></slot></header><main><slot></slot> <!-- 默認插槽 --></main><footer><slot name="footer"></slot></footer></div>
</template>
父組件:
<ChildComponent><template #header><h1>頁面標題</h1></template><p>主內容區域</p><template v-slot:footer><p>版權信息 ? 2023</p></template>
</ChildComponent>
三、作用域插槽(核心進階)
1. 數據傳遞原理
子組件向插槽傳遞數據 → 父組件接收使用
子組件:
<template><ul><li v-for="item in items" :key="item.id"><slot :item="item" :index="index"></slot></li></ul>
</template><script setup>
const items = ref([{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }])
</script>
父組件:
<ChildComponent><template #default="{ item, index }"><span>{{ index + 1 }}. {{ item.name }}</span><button @click="deleteItem(item.id)">刪除</button></template>
</ChildComponent>
2. 應用場景示例
場景:表格組件支持自定義列渲染
子組件:
<template><table><thead><tr><th v-for="col in columns" :key="col.key">{{ col.title }}</th></tr></thead><tbody><tr v-for="row in data" :key="row.id"><td v-for="col in columns" :key="col.key"><slot :name="col.key" :row="row">{{ row[col.key] }} <!-- 默認顯示 --></slot></td></tr></tbody></table>
</template>
父組件:
<DataTable :data="users" :columns="columns"><template #action="{ row }"><button @click="editUser(row.id)">編輯</button><button @click="deleteUser(row.id)">刪除</button></template><template #status="{ row }"><span :class="row.status">{{ row.status | statusText }}</span></template>
</DataTable>
四、動態插槽名
子組件:
<template><div class="dynamic-slot"><slot :name="slotName"></slot></div>
</template><script setup>
defineProps({slotName: {type: String,default: 'default'}
})
</script>
父組件:
<ChildComponent :slotName="currentSlot"><template #[currentSlot]><p>動態插槽內容(當前使用 {{ currentSlot }} 插槽)</p></template>
</ChildComponent>
五、高級技巧
1. 插槽繼承($slots)
訪問子組件插槽內容:
// 子組件內部
const slots = useSlots()
console.log(slots.header()) // 獲取具名插槽內容
2. 渲染函數中使用插槽
// 使用 h() 函數創建元素
export default {render() {return h('div', [this.$slots.default?.() || '默認內容',h('div', { class: 'footer' }, this.$slots.footer?.())])}
}
六、最佳實踐指南
-
命名規范
-
使用小寫字母 + 連字符命名具名插槽(如?
#user-avatar
) -
避免使用保留字作為插槽名(如?
default
、item
)
-
-
性能優化
<!-- 通過 v-if 控制插槽內容渲染 --> <template #header v-if="showHeader"><HeavyComponent /> </template>
-
類型安全(TypeScript)
// 定義作用域插槽類型 defineSlots<{default?: (props: { item: T; index: number }) => anyheader?: () => anyfooter?: () => any }>()
七、常見問題解答
Q1:如何強制要求必須提供某個插槽
// 子組件中驗證
export default {mounted() {if (!this.$slots.header) {console.error('必須提供 header 插槽內容')}}
}
Q2:插槽內容如何訪問子組件方法?
<!-- 子組件暴露方法 -->
<slot :doSomething="handleAction"></slot><!-- 父組件使用 -->
<template #default="{ doSomething }"><button @click="doSomething">觸發子組件方法</button>
</template>
Q3:如何實現插槽內容過渡動畫?
<transition name="fade" mode="out-in"><slot></slot>
</transition>
八、綜合應用案例
可配置的模態框組件
<!-- Modal.vue -->
<template><div class="modal" v-show="visible"><div class="modal-header"><slot name="header"><h2>{{ title }}</h2><button @click="close">×</button></slot></div><div class="modal-body"><slot :close="close"></slot></div><div class="modal-footer"><slot name="footer"><button @click="close">關閉</button></slot></div></div>
</template>
父組件使用:
<Modal v-model:visible="showModal" title="自定義標題"><template #header><h1 style="color: red;">緊急通知</h1></template><template #default="{ close }"><p>確認刪除此項?</p><button @click="confirmDelete; close()">確認</button></template><template #footer><button @click="showModal = false">取消</button></template>
</Modal>