组件化是 Vue 框架的核心灵魂,而组件通信则是组件化开发中最基础、最高频的能力。从 Vue2 到 Vue3 的演进中,组件通信的实现方式发生了诸多变化:部分 API 被废弃,部分能力被优化整合,同时也诞生了更符合现代工程化的新方案。
本文将全面梳理 Vue2 与 Vue3 中所有主流的组件通信方式,结合代码示例讲解用法,并从设计理念、适用场景、开发体验等维度做深度对比,帮你在不同项目场景下选择最合适的通信方案。
一、Vue2 组件通信方式全览
Vue2 基于选项式 API(Options API)设计,提供了丰富且灵活的通信手段,覆盖父子、隔代、兄弟、全局等所有场景。
1. 基础父子通信:props / $emit
这是 Vue 最经典、最符合单向数据流设计的通信方式,也是日常开发中使用频率最高的方案。
- 父传子 :父组件通过自定义属性向下传递数据,子组件用
props声明接收 - 子传父 :子组件通过
$emit触发自定义事件,父组件监听事件并接收参数
代码示例
vue
<!-- 父组件 Parent.vue -->
<template>
<Child
:message="parentMsg"
@update-text="handleUpdate"
/>
</template>
<script>
import Child from './Child.vue'
export default {
components: { Child },
data() {
return { parentMsg: '来自父组件的数据' }
},
methods: {
handleUpdate(val) {
console.log('子组件传来的值:', val)
}
}
}
</script>
vue
<!-- 子组件 Child.vue -->
<template>
<div @click="sendToParent">{{ message }}</div>
</template>
<script>
export default {
props: {
message: {
type: String,
default: ''
}
},
methods: {
sendToParent() {
this.$emit('update-text', '子组件的回传数据')
}
}
}
</script>
补充:Vue2 还提供了
.sync修饰符,是v-model的语法糖扩展,用于简化"子组件修改父组件值"的写法,在 Vue3 中被多v-model替代。
2. 父子实例直接访问:parent / children
允许父子组件直接访问对方的实例对象,从而读取数据、调用方法,使用非常便捷,但破坏了组件封装性。
this.$parent:子组件获取父组件实例this.$children:父组件获取子组件实例数组
注意事项:
$children不保证顺序,也不是响应式的- 深度依赖会导致组件耦合度高,难以维护和复用
3. 隔代属性透传:attrs / listeners
Vue 2.4 版本引入,专门用于解决多层级组件属性/事件透传问题,避免 props 层层传递,非常适合封装高阶组件、二次封装 UI 组件库。
$attrs:包含父作用域中不被 props 识别的特性(class 和 style 除外)$listeners:包含父作用域中所有的v-on事件监听器
典型场景:爷组件 → 父组件(包装层)→ 孙组件,父组件只做透传,不关心具体属性。
vue
<!-- 父组件(中间层)Father.vue -->
<template>
<!-- v-bind="$attrs" 透传属性,v-on="$listeners" 透传事件 -->
<GrandChild v-bind="$attrs" v-on="$listeners" />
</template>
4. 跨层级依赖注入:provide / inject
Vue 2.2.0 新增的 API,用于祖先组件向所有后代组件注入数据,无论层级多深都可以直接获取,完美解决深层级 props 透传的痛点。
provide:祖先组件中定义要提供的数据/方法inject:后代组件中声明要注入的数据
代码示例
javascript
// 爷组件 GrandFather.vue
export default {
provide() {
return {
theme: 'dark',
changeTheme: this.changeTheme
}
},
methods: {
changeTheme(theme) {
this.theme = theme
}
}
}
javascript
// 孙组件 GrandSon.vue
export default {
inject: ['theme', 'changeTheme'],
mounted() {
console.log(this.theme) // 'dark'
}
}
重要注意:Vue2 中
provide/inject默认不是响应式的。只有当传入的是一个响应式对象时,对象内部的属性变化才会触发视图更新。
5. 组件实例引用:ref / $refs
给子组件或 DOM 元素添加 ref 属性,父组件通过 this.$refs.xxx 直接获取其实例,从而调用子组件的方法、访问子组件的数据。
vue
<template>
<Child ref="childComp" />
</template>
<script>
export default {
mounted() {
// 直接调用子组件的方法
this.$refs.childComp.resetForm()
}
}
</script>
注意:
$refs只在组件渲染完成后才填充,且不是响应式的,避免在模板或计算属性中使用。
6. 任意组件通信:EventBus(事件总线)
通过一个空的 Vue 实例作为中央事件总线,所有组件都可以通过它来触发和监听事件,实现任意组件间的通信(兄弟、跨级、无关联组件)。
代码示例
javascript
// eventBus.js
import Vue from 'vue'
export const EventBus = new Vue()
javascript
// 组件A:触发事件
import { EventBus } from './eventBus.js'
EventBus.$emit('sendMsg', '来自A组件的消息')
javascript
// 组件B:监听事件
import { EventBus } from './eventBus.js'
mounted() {
EventBus.$on('sendMsg', (val) => {
console.log(val)
})
},
beforeDestroy() {
// 必须手动销毁,防止内存泄漏
EventBus.$off('sendMsg')
}
局限性:只适合小型项目。大型项目中事件分散、难以追踪,容易导致逻辑混乱和内存泄漏。
7. 全局状态管理:Vuex
Vue 官方的集中式状态管理方案,将所有组件的共享状态抽取到一个全局 store 中统一管理,通过约定的规则修改状态,保证状态变化可追踪。
核心概念:
state:全局状态数据mutations:唯一修改 state 的方式,同步操作actions:处理异步逻辑,提交 mutationgetters:state 的计算属性modules:模块化拆分 store
适合中大型项目,存在大量跨组件共享状态的场景。
二、Vue3 组件通信方式全览
Vue3 基于组合式 API(Composition API)重构,同时推出了 <script setup> 语法糖,通信方式在继承核心思想的基础上,做了大量优化和精简,更强调封装性和规范性。
1. 基础父子通信:defineProps / defineEmits
核心思想依然是「props 向下,事件向上」,但在 <script setup> 中通过编译器宏实现,写法更简洁。
defineProps:声明接收的 props,返回 props 对象defineEmits:声明可触发的事件,返回 emit 函数
代码示例
vue
<!-- 父组件 Parent.vue -->
<template>
<Child
:count="num"
@update-count="handleUpdate"
v-model:title="pageTitle"
/>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const num = ref(10)
const pageTitle = ref('首页')
const handleUpdate = (val) => {
num.value = val
}
</script>
vue
<!-- 子组件 Child.vue -->
<template>
<div>{{ count }} - {{ title }}</div>
<button @click="changeCount">修改</button>
</template>
<script setup>
const props = defineProps({
count: {
type: Number,
default: 0
},
title: String
})
const emit = defineEmits(['update-count', 'update:title'])
const changeCount = () => {
emit('update-count', props.count + 1)
emit('update:title', '新标题')
}
</script>
重要变化:Vue3 中
v-model全面升级,默认属性为modelValue,默认事件为update:modelValue,且支持一个组件上绑定多个 v-model ,正式替代了 Vue2 的.sync修饰符。
2. 隔代属性透传:$attrs
Vue3 做了大幅简化:移除了 $listeners ,将所有事件监听器统一合并到 $attrs 中。
现在 $attrs 包含了所有未被 props 声明的属性、class、style 以及事件监听器,透传只需要写 v-bind="$attrs" 即可。
vue
<!-- 中间层组件 Father.vue -->
<template>
<!-- 一次性透传所有属性和事件 -->
<GrandChild v-bind="$attrs" />
</template>
3. 跨层级依赖注入:provide / inject
Vue3 的组合式 API 对 provide/inject 做了全面增强,是深层级通信的首选方案。
- 支持传递响应式数据(
ref/reactive) - 提供
readonly保护数据,防止子组件意外修改父级状态 - 支持传递函数,实现子组件调用祖先组件的方法
代码示例
vue
<!-- 祖先组件 Ancestor.vue -->
<script setup>
import { ref, provide, readonly } from 'vue'
const theme = ref('dark')
const changeTheme = (newTheme) => {
theme.value = newTheme
}
// 用 readonly 保护数据,子组件只能读取不能直接修改
provide('theme', readonly(theme))
provide('changeTheme', changeTheme)
</script>
vue
<!-- 后代组件 Descendant.vue -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme', 'light') // 第二个参数是默认值
const changeTheme = inject('changeTheme')
</script>
4. 组件实例引用:ref + defineExpose
Vue3 对组件封装做了强化:使用 <script setup> 的组件默认是封闭的 ,外部通过 ref 无法直接访问组件内部的属性和方法。
必须使用 defineExpose 显式暴露需要对外公开的内容。
代码示例
vue
<!-- 子组件 Child.vue -->
<script setup>
import { ref } from 'vue'
const formData = ref({ name: '' })
const resetForm = () => {
formData.value = { name: '' }
}
// 显式暴露给父组件
defineExpose({
resetForm
})
</script>
vue
<!-- 父组件 Parent.vue -->
<template>
<Child ref="childRef" />
<button @click="handleReset">重置表单</button>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
const handleReset = () => {
childRef.value.resetForm()
}
</script>
补充:Vue3 正式移除了
$children;$parent虽然还存在,但官方不推荐使用,更推崇单向数据流和显式暴露的规范。
5. 任意组件通信:mitt / tiny-emitter
Vue3 移除了 Vue 实例上的 $on、$off、$once 等事件方法,因此不再支持原生的 EventBus 写法。
官方推荐使用 mitt、tiny-emitter 等轻量级第三方事件库来实现事件总线功能。
代码示例(mitt)
bash
npm install mitt
javascript
// eventBus.js
import mitt from 'mitt'
export const emitter = mitt()
vue
<!-- 组件A:触发事件 -->
<script setup>
import { emitter } from './eventBus.js'
emitter.emit('sendMsg', '来自A的消息')
</script>
vue
<!-- 组件B:监听事件 -->
<script setup>
import { onUnmounted } from 'vue'
import { emitter } from './eventBus.js'
const handler = (val) => {
console.log(val)
}
emitter.on('sendMsg', handler)
onUnmounted(() => {
emitter.off('sendMsg', handler)
})
</script>
6. 全局状态管理:Pinia
Pinia 是 Vue 官方新一代状态管理库,正式替代 Vuex,完美适配 Vue3 和 TypeScript。
相比 Vuex 的核心优势:
- 去掉了
mutations,直接修改 state,更简洁 - 原生支持组合式 API 写法
- 完美的 TypeScript 类型推导
- 更轻量,支持模块自动拆分
- 支持 devtools 调试、热更新
代码示例
javascript
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// state
const username = ref('张三')
const age = ref(25)
// getters
const fullInfo = computed(() => `${username.value} - ${age.value}岁`)
// actions
const updateAge = (newAge) => {
age.value = newAge
}
return { username, age, fullInfo, updateAge }
})
vue
<!-- 组件中使用 -->
<script setup>
import { useUserStore } from '@/stores/user.js'
const userStore = useUserStore()
// 直接读取
console.log(userStore.username)
console.log(userStore.fullInfo)
// 直接修改或调用方法
userStore.updateAge(26)
userStore.username = '李四'
</script>
7. 补充:defineModel(Vue 3.4+)
Vue 3.4 推出的编译器宏,进一步简化了双向绑定的写法,让组件内部实现 v-model 像声明变量一样简单。
vue
<!-- 子组件 -->
<script setup>
// 等价于同时声明了 modelValue 属性和 update:modelValue 事件
const modelValue = defineModel()
</script>
<template>
<input v-model="modelValue" />
</template>
三、Vue2 vs Vue3 通信方式深度对比
| 通信场景 | Vue2 方案 | Vue3 方案 | 核心差异与变化 |
|---|---|---|---|
| 基础父子通信 | props / $emit、.sync 修饰符 | defineProps / defineEmits、多 v-model | 语法更简洁;多 v-model 替代 .sync;语义更清晰 |
| 隔代透传 | attrs / listeners | $attrs | 移除 listeners,事件统一合并到 attrs,使用更简单 |
| 跨层级注入 | provide / inject(默认非响应式) | provide / inject(原生支持响应式) | Vue3 支持 ref/reactive 响应式传递;支持 readonly 保护;能力大幅增强 |
| 实例访问 | refs、refs、refs、parent、$children | ref + defineExpose、$parent(不推荐) | 移除 $children;默认封闭组件,必须显式 expose,更强调封装性 |
| 事件总线 | 内置 EventBus(基于 Vue 实例) | 移除内置方案,使用 mitt 等第三方库 | Vue3 剥离了事件能力,需引入轻量库,功能一致 |
| 全局状态管理 | Vuex(选项式、mutations/actions 分离) | Pinia(组合式、直接修改 state) | Pinia 更简洁、TS 友好、无冗余概念,是 Vue3 官方标准方案 |
| 设计理念 | 灵活宽松,提供多种直接访问方式 | 规范收敛,强调封装与单向数据流 | Vue3 牺牲了部分便捷性,换来了更好的可维护性和工程化能力 |
四、不同场景下的最佳实践建议
- 父子组件通信 :优先使用
props + emit / v-model,严格遵循单向数据流,这是最规范、最易维护的方式。 - UI 组件二次封装 :使用
$attrs做属性和事件透传,减少重复代码。 - 深层级跨代通信 :优先选择
provide / inject,避免 props 层层传递的"props 地狱"。 - 兄弟/无关联组件 :小型项目用
mitt事件总线;中大型项目统一用Pinia管理共享状态。 - 操作子组件能力 :使用
ref + defineExpose,只暴露必要的方法和属性,不要滥用。 - 全局共享状态:Vue3 项目直接使用 Pinia,不再考虑 Vuex。
结语
从 Vue2 到 Vue3,组件通信的演进本质上是「灵活自由」向「规范工程」的转变。Vue2 提供了大量便捷但容易失控的 API,适合快速开发;Vue3 则收敛了边界,强化了封装和单向数据流,更适合大型团队协作和长期维护的项目。
理解两种版本的设计思路差异,根据项目规模、团队情况选择合适的通信方案,才能写出更优雅、更易维护的 Vue 代码。