组件通信 #
一、通信方式概览 #
text
┌─────────────────────────────────────────────────────┐
│ 父组件 │
│ ┌─────────────────────────────────────────────┐ │
│ │ Props ↓ ↑ Emits │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 子组件 A │ │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ │ Provide ↓ ↑ Inject │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 子组件 B │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 兄弟组件通信:Event Bus / Vuex / Pinia │
└─────────────────────────────────────────────────────┘
二、Props(父传子) #
2.1 基本用法 #
vue
<!-- 父组件 -->
<template>
<Child
title="标题"
:count="count"
:user="user"
/>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const count = ref(0)
const user = ref({ name: '张三', age: 25 })
</script>
vue
<!-- 子组件 Child.vue -->
<template>
<h2>{{ title }}</h2>
<p>计数: {{ count }}</p>
<p>用户: {{ user.name }}</p>
</template>
<script setup>
defineProps({
title: String,
count: Number,
user: Object
})
</script>
2.2 Props验证 #
vue
<script setup>
defineProps({
title: {
type: String,
required: true,
validator(value) {
return value.length > 0
}
},
count: {
type: Number,
default: 0
},
type: {
type: String,
default: 'primary',
validator(value) {
return ['primary', 'success', 'warning', 'danger'].includes(value)
}
}
})
</script>
2.3 使用useAttrs获取未声明的Props #
vue
<template>
<div v-bind="attrs">
<slot></slot>
</div>
</template>
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
// 父组件传递但未在defineProps中声明的属性
</script>
三、Emits(子传父) #
3.1 基本用法 #
vue
<!-- 子组件 Child.vue -->
<template>
<button @click="handleClick">点击</button>
</template>
<script setup>
const emit = defineEmits(['update', 'delete'])
function handleClick() {
emit('update', { id: 1, name: 'updated' })
}
</script>
vue
<!-- 父组件 -->
<template>
<Child
@update="handleUpdate"
@delete="handleDelete"
/>
</template>
<script setup>
import Child from './Child.vue'
function handleUpdate(data) {
console.log('更新:', data)
}
function handleDelete(id) {
console.log('删除:', id)
}
</script>
3.2 事件验证 #
vue
<script setup>
const emit = defineEmits({
submit: (payload) => {
if (!payload.email || !payload.password) {
console.warn('无效的提交数据')
return false
}
return true
}
})
function handleSubmit() {
emit('submit', { email: 'test@example.com', password: '123456' })
}
</script>
3.3 v-model语法糖 #
vue
<!-- 子组件 CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
vue
<!-- 父组件 -->
<template>
<CustomInput v-model="text" />
</template>
四、Provide/Inject(跨层级) #
4.1 基本用法 #
vue
<!-- 祖先组件 -->
<template>
<Child />
</template>
<script setup>
import { provide, ref } from 'vue'
import Child from './Child.vue'
const theme = ref('dark')
const user = ref({ name: '张三' })
provide('theme', theme)
provide('user', user)
function updateTheme(newTheme) {
theme.value = newTheme
}
provide('updateTheme', updateTheme)
</script>
vue
<!-- 后代组件(任意层级) -->
<template>
<div :class="theme">
用户: {{ user.name }}
<button @click="changeTheme">切换主题</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
const user = inject('user')
const updateTheme = inject('updateTheme')
function changeTheme() {
updateTheme(theme.value === 'dark' ? 'light' : 'dark')
}
</script>
4.2 默认值 #
vue
<script setup>
import { inject } from 'vue'
// 提供默认值
const theme = inject('theme', 'light')
// 使用工厂函数作为默认值
const config = inject('config', () => ({
apiUrl: '/api',
timeout: 5000
}))
</script>
4.3 响应式Provide #
vue
<script setup>
import { provide, ref, readonly } from 'vue'
const state = ref({
count: 0,
user: null
})
// 提供只读状态
provide('state', readonly(state))
// 提供修改方法
provide('actions', {
increment() {
state.value.count++
},
setUser(user) {
state.value.user = user
}
})
</script>
4.4 使用Symbol作为key #
javascript
// keys.js
export const ThemeKey = Symbol('theme')
export const UserKey = Symbol('user')
vue
<script setup>
import { provide, inject } from 'vue'
import { ThemeKey, UserKey } from './keys'
// 提供
provide(ThemeKey, 'dark')
// 注入
const theme = inject(ThemeKey)
</script>
五、Event Bus(非父子通信) #
5.1 创建Event Bus #
javascript
// eventBus.js
import { ref } from 'vue'
class EventBus {
constructor() {
this.events = {}
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
}
emit(event, ...args) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(...args))
}
}
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback)
}
}
}
export const eventBus = new EventBus()
5.2 使用Event Bus #
vue
<!-- 组件A - 发送事件 -->
<script setup>
import { eventBus } from './eventBus'
function sendMessage() {
eventBus.emit('message', 'Hello from A')
}
</script>
vue
<!-- 组件B - 接收事件 -->
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { eventBus } from './eventBus'
function handleMessage(msg) {
console.log('收到消息:', msg)
}
onMounted(() => {
eventBus.on('message', handleMessage)
})
onUnmounted(() => {
eventBus.off('message', handleMessage)
})
</script>
5.3 使用mitt库(推荐) #
bash
npm install mitt
javascript
// eventBus.js
import mitt from 'mitt'
export const emitter = mitt()
vue
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { emitter } from './eventBus'
onMounted(() => {
emitter.on('message', handleMessage)
})
onUnmounted(() => {
emitter.off('message', handleMessage)
})
function handleMessage(msg) {
console.log(msg)
}
function sendMessage() {
emitter.emit('message', 'Hello')
}
</script>
六、$parent和$root #
6.1 访问父组件 #
vue
<script setup>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
function callParentMethod() {
// 访问父组件
instance.parent?.exposed?.someMethod()
}
</script>
6.2 访问根组件 #
vue
<script setup>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
function callRootMethod() {
// 访问根组件
instance.root?.exposed?.globalMethod()
}
</script>
七、组件通信最佳实践 #
7.1 选择指南 #
| 场景 | 推荐方式 |
|---|---|
| 父传子 | Props |
| 子传父 | Emits |
| 跨层级 | Provide/Inject |
| 兄弟组件 | Pinia/Vuex |
| 任意组件 | Pinia/Vuex |
| 轻量级兄弟通信 | Event Bus |
7.2 完整示例:用户管理 #
vue
<!-- 祖先组件 App.vue -->
<template>
<div>
<UserList />
<UserDetail />
</div>
</template>
<script setup>
import { provide, ref } from 'vue'
import UserList from './UserList.vue'
import UserDetail from './UserDetail.vue'
const selectedUser = ref(null)
const users = ref([
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' }
])
provide('users', users)
provide('selectedUser', selectedUser)
provide('selectUser', (user) => {
selectedUser.value = user
})
</script>
vue
<!-- UserList.vue -->
<template>
<ul>
<li v-for="user in users" :key="user.id" @click="selectUser(user)">
{{ user.name }}
</li>
</ul>
</template>
<script setup>
import { inject } from 'vue'
const users = inject('users')
const selectUser = inject('selectUser')
</script>
vue
<!-- UserDetail.vue -->
<template>
<div v-if="selectedUser">
<h3>{{ selectedUser.name }}</h3>
<p>{{ selectedUser.email }}</p>
</div>
</template>
<script setup>
import { inject } from 'vue'
const selectedUser = inject('selectedUser')
</script>
八、总结 #
| 方式 | 方向 | 适用场景 |
|---|---|---|
| Props | 父→子 | 数据传递 |
| Emits | 子→父 | 事件通知 |
| v-model | 双向 | 表单组件 |
| Provide/Inject | 跨层级 | 深层嵌套 |
| Event Bus | 任意 | 轻量通信 |
| Pinia/Vuex | 全局 | 状态管理 |
通信原则:
- 遵循单向数据流
- 优先使用Props和Emits
- 深层嵌套使用Provide/Inject
- 复杂状态使用Pinia/Vuex
- 避免过度使用Event Bus
最后更新:2026-03-26