我们将系统讲解以下几种通信方式:
| 场景 | 通信方向 | 常用机制 |
|---|---|---|
| 父 → 子 | 父组件传数据给子组件 | props |
| 子 → 父 | 子组件传事件/数据给父组件 | emit |
| 父 ↔ 子 | 父子互相修改数据 | v-model(语法糖) |
| 深层通信 | 任意组件层级间传值 | provide / inject |
| 兄弟组件 | 无父子关系的通信 | 事件总线 / 状态管理(Pinia / Vuex) |
一、父传子 --- props
✅ 基础用法
父组件:
html
<template>
<UserCard :name="userName" :age="userAge" />
</template>
<script setup>
import UserCard from './UserCard.vue'
const userName = 'Alice'
const userAge = 25
</script>
子组件:
html
<script setup>
const props = defineProps({
name: String,
age: Number
})
</script>
<template>
<div>{{ name }} --- {{ age }}岁</div>
</template>
🧠 解释:
-
defineProps()是 Vue 3<script setup>提供的编译宏,用于声明接收的属性。 -
传递数据时建议加
:(动态绑定),否则会被当作字符串。
⚙️ 默认值 & 类型验证
html
<script setup>
const props = defineProps({
name: {
type: String,
default: '未命名'
},
age: {
type: Number,
required: true
}
})
</script>
-
default:提供默认值 -
required:必传属性 -
type:类型限制(调试模式下会警告)
⚠️ 单向数据流原则
子组件不应直接修改 props!
错误示例:
javascript
props.age++ // ❌
正确做法是复制:
javascript
const localAge = ref(props.age)
或者通过事件让父组件更新(见下一节)。
二、子传父 --- emit
✅ 基本示例
子组件:
html
<script setup>
const emit = defineEmits(['update'])
function handleClick() {
emit('update', '来自子组件的新数据')
}
</script>
<template>
<button @click="handleClick">通知父组件</button>
</template>
父组件:
html
<template>
<Child @update="onUpdate" />
</template>
<script setup>
import Child from './Child.vue'
function onUpdate(data) {
console.log('收到子组件数据:', data)
}
</script>
🧠 解释:
-
子组件定义
defineEmits(['update']) -
使用
emit('update', payload)触发事件 -
父组件用
@update监听
⚙️ 事件参数校验(Vue 3.3+ 新特性)
javascript
const emit = defineEmits({
update(payload) {
if (typeof payload !== 'string') {
console.warn('参数必须是字符串')
return false
}
return true
}
})
三、父子双向通信 --- v-model
Vue 3 中 v-model 是 props + emit 的语法糖。
✅ 基础示例
父组件:
html
<template>
<Child v-model="message" />
<p>父组件数据: {{ message }}</p>
</template>
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const message = ref('Hello')
</script>
子组件:
html
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
function change() {
emit('update:modelValue', props.modelValue + ' Vue3')
}
</script>
<template>
<button @click="change">修改父组件数据</button>
</template>
🧠 底层机制:
| 父组件写法 | 子组件内部等价 |
|---|---|
v-model="data" |
:modelValue="data" @update:modelValue="data = $event" |
⚙️ 多个 v-model
支持多个绑定字段:
父组件:
html
<Child v-model:title="title" v-model:content="content" />
子组件:
html
<script setup>
const props = defineProps(['title', 'content'])
const emit = defineEmits(['update:title', 'update:content'])
</script>
四、跨层级通信 --- provide / inject
当父组件与子孙组件之间层级较深时,可以使用 依赖注入机制。
祖先组件:
html
<script setup>
import { provide, ref } from 'vue'
const user = ref('Tom')
provide('user', user)
</script>
<template>
<ChildA />
</template>
任意后代组件:
html
<script setup>
import { inject } from 'vue'
const user = inject('user')
</script>
<template>
<p>用户: {{ user }}</p>
</template>
🧠 特点:
-
非响应式变量建议用
ref包装; -
后代组件可以修改注入的值;
-
常用于全局配置、主题共享、依赖注入(如表单上下文)。
五、兄弟组件通信方案
Vue 没有直接的兄弟通信,需要借助:
1️⃣ 事件总线(Event Bus)
手动创建一个全局事件实例(简单项目可用):
html
// eventBus.js
import mitt from 'mitt'
export default mitt()
使用:
html
// 组件A
import bus from './eventBus'
bus.emit('change', 123)
// 组件B
import bus from './eventBus'
bus.on('change', val => console.log(val))
2️⃣ 全局状态管理(Pinia / Vuex)
中大型项目推荐使用 Pinia 管理全局状态:
html
npm install pinia
html
// store.js
import { defineStore } from 'pinia'
export const useMainStore = defineStore('main', {
state: () => ({ count: 0 }),
})
六、总结对比表
| 方式 | 方向 | 优点 | 缺点 | 场景 |
|---|---|---|---|---|
props |
父 → 子 | 简单直观 | 单向,不能反向修改 | 普通父传子 |
emit |
子 → 父 | 清晰、解耦 | 只能往上一级传 | 按钮点击、表单回传 |
v-model |
父 ↔ 子 | 双向绑定,简洁 | 内部实现较复杂 | 表单组件 |
provide/inject |
任意层级 | 深层传值方便 | 调试不便 | 全局配置/依赖注入 |
| 事件总线 / Pinia | 任意组件 | 解耦、灵活 | 管理复杂 | 大型项目、兄弟通信 |