组件通信是 Vue 开发的核心命题,本文将从父子通信、子父通信、兄弟通信三个维度,深入剖析 Vue 3 的 7 种通信方式,并提供代码示例与场景指南。
一、父子组件通信
1.1 Props 传值(推荐)
实现方式: 父组件用 v-bind
传递数据,子组件用 defineProps
接收
html
<!-- 父组件 -->
<Child :title="pageTitle" :list="dataList" />
<!-- 子组件 -->
<script setup>
const props = defineProps({
title: String,
list: {
type: Array,
default: () => [] // 默认值用函数返回
}
})
</script>
注意事项:
- 遵循单向数据流原则,禁止直接修改
props
- 对象/数组的默认值必须从工厂函数返回
1.2 Provide/Inject(跨层级)
实现方案: 父组件提供数据,后代组件注入数据
html
<!-- 祖先组件 -->
<script setup>
import { provide, readonly, ref } from 'vue'
const counter = ref(0)
// 使用 readonly 防止意外修改
provide('globalCounter', readonly(counter))
</script>
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
// 注入数据
const counter = inject('globalCounter')
</script>
使用场景:
主题配置、多级嵌套表单等跨层级数据共享
二、子父组件通信
2.1 自定义事件(标准方案)
实现方式: 子组件定义并触发事件,父组件订阅
html
<!-- 子组件 -->
<script setup>
const emit = defineEmits(['update:title']) // 修正拼写错误
const handleClick = () => {
emit('update:title', 'New Title') // 触发事件
}
</script>
<!-- 父组件 -->
<Child @update:title="handleUpdate" />
2.2 暴露组件实例
实现方案: 子组件暴露数据,父组件通过 ref
访问
html
<!-- 子组件 -->
<script setup>
const list = ref([])
// 暴露数据给父组件
defineExpose({ list })
</script>
<!-- 父组件 -->
<template>
<Child ref="childRef" />
</template>
<script setup>
const childRef = ref(null)
// 访问子组件数据
console.log(childRef.value?.list)
</script>
2.3 双向绑定(v-model 增强)
多字段绑定实现: 父组件通过 v-model:xxx
绑定数据,子组件接收并触发更新事件
html
<!-- 父组件:绑定多个字段 -->
<UserForm
v-model:username="formData.name"
v-model:age="formData.age"
/>
<!-- 子组件:接收并触发更新 -->
<template>
<input
:value="username"
@input="$emit('update:username', $event.target.value)"
>
<input
:value="age"
@input="$emit('update:age', $event.target.value)"
>
</template>
<script setup>
// 接收父组件数据
defineProps(['username', 'age'])
// 定义可触发的事件
defineEmits(['update:username', 'update:age'])
</script>
核心要点:
- 本质是
props
+update:xxx
事件的语法糖 - 必须显式触发
update:xxx
事件 - 支持同时绑定多个字段(优于单个 v-model)
- 适用于表单控件封装等场景
三、兄弟组件通信
3.1 共享状态模式
实现方案: 共享响应式状态对象
javascript
// store.js
import { reactive } from 'vue'
export const sharedState = reactive({ count: 0 })
// ComponentA.vue
sharedState.count++ // 修改状态
// ComponentB.vue
console.log(sharedState.count) // 读取状态
3.2 Pinia 状态管理(推荐)
Store 定义:
javascript
// stores/counter.js
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++
}
}
})
组件使用:
html
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
<template>
<button @click="counter.increment">{{ counter.count }}</button>
</template>
四、七种通信方案对比
通信方式 | 实现形式 | 数据流向 | 适用场景 | 优点 | 缺点 |
---|---|---|---|---|---|
Props 传值 | v-bind + defineProps |
父→子 | 父子数据传递 | 类型安全/单向数据流 | 多层传递繁琐 |
v-model 双向绑定 | v-model:prop 语法糖 |
父子双向 | 表单组件/双向绑定 | 简化代码/标准语法 | 需严格定义 update 事件 |
自定义事件 | defineEmits + v-on |
子→父 | 子组件通知父组件 | 显式数据流 | 需维护事件名 |
组件实例暴露 | defineExpose + ref |
父访问子 | 调用子组件方法/状态 | 灵活 | 破坏封装性 |
Provide/Inject | provide + inject |
祖先→后代 | 跨层级共享 | 避免逐层传递 | 数据流向不透明 |
共享状态对象 | 响应式对象模块化 | 任意组件 | 简单兄弟通信 | 实现简单 | 难以维护 |
Pinia 状态库 | defineStore + useStore |
全局 | 复杂应用状态管理 | 调试工具支持 | 需学习额外 API |
五、最佳实践指南
1. 选择策略
- 简单场景:父子用 Props/Events,兄弟用共享对象
- 复杂场景:跨层级用 Provide/Inject,全局状态用 Pinia
2. 安全规范
javascript
// Provide 响应式数据示例
const data = ref({})
provide('safeData', readonly(data)) // 只读保护
// 事件命名规范
defineEmits(['submit-form', 'cancel-action']) // 统一命名风格
3. 性能优化
- 大对象传递时使用
shallowRef/shallowReactive
- 频繁更新的数据优先用 Pinia(利用其性能优化机制)