Vue 2 组件通信方式
大白话解释: 组件通信就像"家庭成员之间怎么传递信息":
- 父子通信(Props/Events):父母给孩子东西(Props),孩子告诉父母事情(Events)
- 兄弟通信(EventBus):兄弟之间通过共同的朋友传话(事件总线)
- 跨层级通信(Provide/Inject):爷爷直接给孙子东西,不用经过爸爸
- 全局状态(Vuex):家里有一个公共记账本,谁都能看和改
什么时候用哪种方式?
- 父子组件:用 Props/Events(最简单、最推荐)
- 兄弟组件:用 EventBus 或 Vuex
- 跨层级组件:用 Provide/Inject
- 复杂状态管理:用 Vuex
1. Props / Events(父子组件)
最基础、最推荐的通信方式。
Props —— 父传子
<!-- 父组件 -->
<template>
<UserCard
:name="userName"
:age="userAge"
:avatar="userAvatar"
:tags="userTags"
/>
</template>
<script>
export default {
data() {
return {
userName: '张三',
userAge: 25,
userAvatar: '/avatars/1.jpg',
userTags: ['前端', 'Vue', 'React'],
}
},
}
</script><!-- 子组件 -->
<script>
export default {
props: {
name: { type: String, required: true },
age: { type: Number, default: 0 },
avatar: { type: String, default: '/avatars/default.jpg' },
tags: {
type: Array,
default() { return [] },
},
},
}
</script>Events —— 子传父
<!-- 子组件 -->
<script>
export default {
methods: {
handleDelete() {
// 触发 delete 事件,把 id 传给父组件
this.$emit('delete', this.item.id)
},
handleEdit() {
// 触发 edit 事件,把对象传给父组件
this.$emit('edit', { id: this.item.id, name: this.item.name })
},
},
}
</script><!-- 父组件 -->
<template>
<UserCard
:user="currentUser"
@delete="onDelete"
@edit="onEdit"
/>
</template>
<script>
export default {
methods: {
onDelete(id) {
this.userList = this.userList.filter((u) => u.id !== id)
},
onEdit(payload) {
this.editingUser = payload
},
},
}
</script>v-model —— 双向绑定
⚠️ Vue 3 差异:Vue 2 的
v-model默认使用valueprop 和input事件;Vue 3 改为modelValueprop 和update:modelValue事件。Vue 2 的model选项在 Vue 3 中被移除,改用参数化v-model:title="val"语法。
<!-- 父组件 -->
<CustomInput v-model="searchText" />
<!-- Vue 2 等价于 -->
<CustomInput :value="searchText" @input="searchText = $event" />
<!-- Vue 3 等价于 -->
<!-- <CustomInput :modelValue="searchText" @update:modelValue="searchText = $event" /> --><!-- 子组件 -->
<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>
<script>
export default {
model: {
prop: 'value', // 默认是 value
event: 'input', // 默认是 input
},
props: ['value'],
}
</script>💡
model选项可以自定义 prop 名和事件名,例如 checkbox 用prop: 'checked', event: 'change'。Vue 3 中对应写法为v-model:checked="val"。
2. .sync 修饰符
⚠️ Vue 3 已移除
.sync修饰符。Vue 3 中统一使用带参数的v-model替代,如v-model:visible="showDialog",等价于 Vue 2 的:visible.sync="showDialog"。
<!-- 父组件(Vue 2) -->
<Dialog :visible.sync="showDialog" />
<!-- 等价于 -->
<Dialog :visible="showDialog" @update:visible="showDialog = $event" />
<!-- Vue 3 等价写法 -->
<!-- <Dialog v-model:visible="showDialog" /> --><!-- 子组件 -->
<script>
export default {
props: ['visible'],
methods: {
close() {
this.$emit('update:visible', false)
},
},
}
</script>💡
.sync可以同时修饰多个属性::title.sync="t" :visible.sync="v"。Vue 3 中对应为v-model:title="t" v-model:visible="v"。
3. $refs —— 父直接访问子
<!-- 父组件 -->
<template>
<ChildForm ref="childForm" />
<button @click="submitAll">提交</button>
</template>
<script>
export default {
methods: {
submitAll() {
// 调用子组件方法
const isValid = this.$refs.childForm.validate()
if (isValid) {
const formData = this.$refs.childForm.getFormData()
this.submit(formData)
}
},
},
}
</script><!-- 子组件 -->
<script>
export default {
data() {
return { form: { name: '', email: '' } }
},
methods: {
validate() {
// 表单校验逻辑
return this.form.name && this.form.email
},
getFormData() {
return { ...this.form }
},
},
}
</script>⚠️
$refs不是响应式的,不要在模板或 computed 中依赖它。只在事件处理或 mounted 中使用。
4. $parent / $children
// 子组件中访问父组件
this.$parent.someMethod()
this.$parent.someData
// 父组件中访问子组件(不推荐,用 $refs 代替)
this.$children[0].someMethod() // 顺序不确定⚠️ Vue 3 已移除
$children。Vue 3 中不再暴露子组件实例数组,应统一使用$refs访问子组件。$parent仍可用但同样不推荐。
⚠️ 强烈不推荐。紧耦合,难以维护和测试。优先用 props/events/$refs。
5. provide / inject(跨层级)
祖先组件向任意深度的后代组件传递数据,无需逐层传递 props。
// 祖先组件
export default {
provide() {
return {
// ⚠️ 非响应式 —— 传的是值,不是引用
theme: this.theme,
// ✅ 响应式方案一:传递整个响应式对象
appConfig: this.config, // config 是一个响应式对象
// ✅ 响应式方案二:函数式
getTheme: () => this.theme,
}
},
data() {
return {
theme: 'dark',
config: { primaryColor: '#1890ff', fontSize: 14 },
}
},
}// 后代组件(任意层级)
export default {
inject: ['theme', 'appConfig', 'getTheme'],
computed: {
// 通过函数式获取响应式数据
currentTheme() {
return this.getTheme()
},
// 直接访问响应式对象
primaryColor() {
return this.appConfig.primaryColor
},
},
}provide/inject 的坑
// ❌ 非响应式 —— 祖先的 theme 变了,后代的 this.theme 不会变
provide() {
return { theme: this.theme } // 传的是基本类型的值
}
// ✅ 响应式 —— 传对象引用
provide() {
return { themeConfig: this.themeConfig } // 传的是对象引用
}6. EventBus(事件总线)
⚠️ Vue 3 中 EventBus 不再可用。Vue 3 移除了
$on/$off/$once实例方法,无法通过new Vue()创建事件总线。推荐使用第三方库 mitt 或 tiny-emitter 替代。
任意组件间通过事件通信,适合简单场景。
创建
// main.js
Vue.prototype.$bus = new Vue()
// 或单独文件
// utils/eventBus.js
import Vue from 'vue'
export default new Vue()使用
<!-- 组件 A —— 发送方 -->
<script>
export default {
methods: {
login() {
// 登录成功后
this.$bus.$emit('user:login', {
id: 1,
name: '张三',
token: 'xxx',
})
},
},
}
</script><!-- 组件 B —— 接收方 -->
<script>
export default {
created() {
this.$bus.$on('user:login', this.handleLogin)
},
beforeDestroy() {
// ✅ 必须取消监听,否则内存泄漏
this.$bus.$off('user:login', this.handleLogin)
},
methods: {
handleLogin(user) {
console.log(`${user.name} 登录了`)
this.fetchNotifications()
},
},
}
</script>监听一次
// 只触发一次
this.$bus.$once('app:ready', () => {
console.log('应用准备就绪')
})取消所有监听
// 取消某个事件的所有监听
this.$bus.$off('user:login')
// 取消所有事件的所有监听(慎用)
this.$bus.$off()⚠️ EventBus 的问题:事件名容易拼错、难以追踪数据流、大项目会变成"事件地狱"。中大型项目建议用 Vuex。
7. Vuex(全局状态管理)
适合多组件共享的复杂状态,详见 Vuex 状态管理。
8. $attrs / $listeners(跨层级透传)
⚠️ Vue 3 已移除
$listeners。Vue 3 中事件监听器已合并到$attrs中,不再需要单独访问$listeners。$attrs现在包含所有未被 props 声明的属性(包括 class、style 和事件监听器)。
将父组件的非 props 属性和事件监听器透传给深层子组件。
<!-- 祖先组件 -->
<BaseInput type="text" placeholder="请输入" @focus="onFocus" /><!-- 中间组件 BaseInput.vue -->
<template>
<!-- v-bind="$attrs" 透传非 props 属性 -->
<!-- v-on="$listeners" 透传事件监听 -->
<input v-bind="$attrs" v-on="$listeners" class="base-input" />
</template>
<script>
export default {
inheritAttrs: false, // 不自动应用到根元素
}
</script><!-- 深层子组件中也可以继续透传 -->
<template>
<div class="wrapper">
<InnerInput v-bind="$attrs" v-on="$listeners" />
</div>
</template>9. 兄弟组件通信方案对比
方案一:通过共同父组件中转
<!-- 父组件 -->
<template>
<ChildA @message="onMessage" />
<ChildB :message="messageFromA" />
</template>
<script>
export default {
data() { return { messageFromA: '' } },
methods: {
onMessage(msg) { this.messageFromA = msg },
},
}
</script>方案二:EventBus
// 兄弟 A
this.$bus.$emit('brother:msg', data)
// 兄弟 B
this.$bus.$on('brother:msg', (data) => { ... })方案三:Vuex
// 兄弟 A
this.$store.commit('SET_MESSAGE', data)
// 兄弟 B
computed: {
message() { return this.$store.state.message }
}方案选择指南
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 父 → 子 | props | 最简单,类型安全 |
| 子 → 父 | $emit / .sync | 单向数据流 |
| 父访问子 | $refs | 直接调用子组件方法 |
| 祖先 → 后代 | provide/inject | 避免逐层传递 props |
| 兄弟组件 | 父组件中转 / EventBus / Vuex | 按复杂度选择 |
| 任意组件 | Vuex | 大型项目,状态可追踪 |
| 跨层级透传 | $attrs / $listeners | 包装组件场景 |