一、Vue3 组件通信核心概念
1. 父组件 → 子组件 (Props)
- 数据流向:父组件传递数据给子组件
- 声明位置:在子组件中声明props
- 触发位置:父组件中通过属性传递值
<!-- 父组件 Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const parentData = ref("来自父组件的数据")
</script>
<template>
<Child :message="parentData" />
</template>
<!-- 子组件 Child.vue -->
<script setup>
defineProps({
message: String // 声明接收的prop
})
</script>
<template>
<div class="child">{{ message }}</div>
</template>
2. 子组件 → 父组件 (自定义事件)
- 数据流向:子组件传递数据给父组件
- 声明位置:在子组件中通过
defineEmits
声明事件 - 触发位置:父组件中通过
@事件名
监听
vue
<!-- 子组件 Child.vue -->
<script setup>
// 声明自定义事件
const emit = defineEmits(['child-event'])
function sendData() {
// 触发自定义事件
emit('child-event', '来自子组件的数据')
}
</script>
<template>
<button @click="sendData" class="btn">发送数据到父组件</button>
</template>
<!-- 父组件 Parent.vue -->
<script setup>
function handleData(data) {
alert(`接收到子组件数据: ${data}`)
}
</script>
<template>
<Child @child-event="handleData" />
</template>
3. 兄弟组件通信 (通过共同父组件)
- 数据流向:组件A → 父组件 → 组件B
- 实现方式:结合props和自定义事件
<!-- 父组件 Parent.vue -->
<script setup>
import { ref } from 'vue'
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
const sharedData = ref(null)
function handleData(data) {
sharedData.value = data
}
</script>
<template>
<div class="sibling-container">
<ComponentA @send-data="handleData" />
<ComponentB :data="sharedData" />
</div>
</template>
<!-- 组件A.vue -->
<script setup>
const emit = defineEmits(['send-data'])
function send() {
emit('send-data', '来自组件A的数据')
}
</script>
<template>
<div class="component-a">
<button @click="send">发送数据给组件B</button>
</div>
</template>
<!-- 组件B.vue -->
<script setup>
defineProps(['data'])
</script>
<template>
<div class="component-b">
<div v-if="data">接收到: {{ data }}</div>
<div v-else>等待数据...</div>
</div>
</template>
二、Vue3 特有通信方式
1. provide/inject (依赖注入)
- 跨层级组件通信
- 祖先组件提供数据,后代组件注入使用
<!-- 祖先组件 Ancestor.vue -->
<script setup>
import { provide, ref } from 'vue'
import Parent from './Parent.vue'
const theme = ref('dark')
// 提供数据给所有后代组件
provide('theme', theme)
function toggleTheme() {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
</script>
<template>
<div :class="['ancestor', theme]">
<button @click="toggleTheme">切换主题</button>
<Parent />
</div>
</template>
<!-- 后代组件 Descendant.vue -->
<script setup>
import { inject } from 'vue'
// 注入祖先组件提供的数据
const theme = inject('theme')
</script>
<template>
<div :class="['descendant', theme]">
当前主题: {{ theme }}
</div>
</template>
2. 模板引用 (ref)
- 直接访问子组件实例
<!-- 父组件 Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
function callChildMethod() {
if (childRef.value) {
childRef.value.childMethod()
}
}
</script>
<template>
<Child ref="childRef" />
<button @click="callChildMethod">调用子组件方法</button>
</template>
<!-- 子组件 Child.vue -->
<script setup>
defineExpose({
childMethod: () => {
alert('子组件方法被调用!')
}
})
</script>
<template>
<div class="child">子组件</div>
</template>
3. 事件总线替代方案 (mitt)
- 用于非父子组件通信
// eventBus.js
import mitt from 'mitt'
export const emitter = mitt()
// 组件A (发送事件)
import { emitter } from './eventBus'
emitter.emit('global-event', '全局数据')
// 组件B (接收事件)
import { emitter } from './eventBus'
emitter.on('global-event', (data) => {
console.log('收到全局事件:', data)
})
三、事件声明与触发规则
事件类型 | 声明位置 | 触发位置 | Vue3 语法 |
---|---|---|---|
原生DOM事件 | 子组件模板 | 子组件内部 | <button @click="..."> |
自定义事件 | 子组件中使用 defineEmits | 子组件内部 | emit('event-name', data) |
Props传递事件 | 父组件模板 | 父组件中 | <Child @custom="handler"/> |
全局事件 | 任意组件 | 任意组件 | mitt/emitter |
四、最佳实践与常见问题
1. 保持单向数据流
// 子组件中不要直接修改props
defineProps(['initialValue'])
// 正确做法:触发事件让父组件修改
const emit = defineEmits(['update:value'])
emit('update:value', newValue)
// 父组件使用v-model
<Child v-model:value="parentValue" />
2. 组合式函数复用逻辑
// useCounter.js
import { ref } from 'vue'
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
const decrement = () => count.value--
return { count, increment, decrement }
}
// 组件中使用
<script setup>
import { useCounter } from './useCounter'
const { count, increment } = useCounter()
</script>
3. 事件声明规范
- 使用kebab-case命名事件:
@update-value
- 在子组件顶部使用
defineEmits
声明事件 - 对于复杂数据类型,使用TypeScript定义事件载荷
<script setup lang="ts">
// 使用TypeScript定义事件
const emit = defineEmits<{
(e: 'update:user', user: User): void
(e: 'delete', id: number): void
}>()
</script>
4. 常见问题解答
事件应该在哪个组件声明?
- 原生事件:在触发它的组件中声明
- 自定义事件:在触发它的组件中声明(使用
defineEmits
),在需要响应它的组件中监听
多个组件需要共享数据怎么办?
- 使用provide/inject跨层级传递
- 使用Pinia/Vuex进行状态管理
- 使用组合式函数复用逻辑
子组件可以直接修改props吗?
不可以!应该:
- 使用v-model语法糖
- 通过自定义事件通知父组件修改