组件通信原理
约 1598 字大约 5 分钟
vue组件通信
2026-04-16
1. 组件通信概述
在 Vue 应用中,组件之间需要进行数据传递和事件通信。由于 Vue 采用组件化架构,数据流动遵循单向数据流原则,但实际业务场景中,组件之间的关系错综复杂,因此需要多种通信方式来解决不同场景下的数据交互问题。
组件通信的核心诉求包括:
- 父子组件之间的数据传递
- 跨级组件之间的数据共享
- 任意组件之间的状态同步
- 事件传递与回调执行
2. Vue 2 vs Vue 3 对比
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 父子通信 | props + $emit / $parent / $children | props + defineProps / defineEmits |
| 跨级通信 | provide / inject / event bus | provide / inject |
| 状态管理 | Vuex | Pinia |
| 事件总线 | Vue.prototype.$bus / event bus | mitt / tiny-emitter |
| 组合式 API | 无 | defineProps / defineEmits / inject |
3. 父子组件通信
3.1 props / emits(通用方式)
props 用于父向子传递数据,emits 用于子向父传递事件。
// 父组件
<template>
<ChildComponent
:message="parentMessage"
:count="parentCount"
@update="handleUpdate"
/>
</template>
<script>
export default {
data() {
return {
parentMessage: 'Hello from parent',
parentCount: 0
}
},
methods: {
handleUpdate(newCount) {
this.parentCount = newCount
}
}
}
</script>// 子组件
export default {
props: {
message: {
type: String,
required: true
},
count: {
type: Number,
default: 0
}
},
emits: ['update'],
methods: {
increment() {
this.$emit('update', this.count + 1)
}
}
}3.2 $parent / $children(Vue 2)
Vue 2 中,可以通过 parent访问父组件实例,children 访问子组件实例数组。
// 子组件中访问父组件
this.$parent.parentMethod()
// 父组件中访问子组件
this.$children[0].childMethod()注意
这种方式容易导致组件耦合过紧,不推荐在复杂应用中使用。
3.3 defineProps / defineEmits(Vue 3 Script Setup)
Vue 3 组合式 API 提供了更简洁的定义方式。
<template>
<div>{{ message }} - {{ count }}</div>
<button @click="increment">递增</button>
</template>
<script setup>
const props = defineProps({
message: String,
count: {
type: Number,
default: 0
}
})
const emit = defineEmits(['update'])
const increment = () => {
emit('update', props.count + 1)
}
</script>使用泛型定义类型:
const props = defineProps<{
message: string
count?: number
}>()
const emit = defineEmits<{
(e: 'update', value: number): void
}>()4. 跨级通信
4.1 Provide / Inject
provide/inject 用于跨级组件之间的数据共享,避免层层传递 props。
// 祖先组件
export default {
provide() {
return {
// 简单值
theme: 'dark',
// 响应式数据(需要包装为函数)
user: () => this.user
}
},
data() {
return {
user: { name: 'ZhenYu', age: 18 }
}
}
}// 后代组件
export default {
inject: ['theme', 'user'],
created() {
console.log(this.theme) // 'dark'
console.log(this.user().name) // 'ZhenYu'
}
}Vue 3 中的写法:
<script setup>
import { provide, inject } from 'vue'
// 提供数据
provide('theme', 'dark')
provide('user', reactive({ name: 'ZhenYu', age: 18 }))
// 注入数据
const theme = inject('theme')
const user = inject('user')
</script>4.2 Event Bus(Vue 2)
Vue 2 中可以使用事件总线进行跨组件通信。
// 初始化事件总线
Vue.prototype.$bus = new Vue()
// 组件 A 监听事件
this.$bus.$on('eventName', (data) => {
console.log('Received:', data)
})
// 组件 B 触发事件
this.$bus.$emit('eventName', { message: 'Hello' })
// 组件 A 取消监听
this.$bus.$off('eventName')注意
Vue 3 中移除了 on、off 等事件相关 API,不再支持这种事件总线模式,需要使用第三方库如 mitt。
5. 任意组件通信
5.1 mitt / tiny-emitter
mitt 是一个轻量级的事件发布/订阅库,兼容 Vue 3。
import mitt from 'mitt'
const emitter = mitt()
// 监听事件
emitter.on('user-login', (user) => {
console.log('User logged in:', user)
})
// 触发事件
emitter.emit('user-login', { name: 'ZhenYu' })
// 取消监听
emitter.off('user-login')
// 清空所有监听
emitter.all.clear()在 Vue 3 中,可以在插件中挂载到 app.config.globalProperties:
import { createApp } from 'vue'
import mitt from 'mitt'
const app = createApp(App)
const emitter = mitt()
app.config.globalProperties.$emitter = emitter5.2 Pinia / Vuex
Pinia(Vue 3)和 Vuex(Vue 2)是官方推荐的状态管理方案。
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
name: '',
token: ''
}),
actions: {
login(userInfo) {
this.name = userInfo.name
this.token = userInfo.token
},
logout() {
this.name = ''
this.token = ''
}
}
})<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 登录
userStore.login({ name: 'ZhenYu', token: 'abc123' })
// 在其他组件中访问
console.log(userStore.name)
</script>5.3 全局事件总线封装
可以封装一个全局事件管理器:
// utils/eventBus.js
import mitt from 'mitt'
const emitter = mitt()
export default {
install(app) {
app.config.globalProperties.$bus = {
on: emitter.on,
off: emitter.off,
emit: emitter.emit,
all: emitter.all
}
}
}6. 常见场景与最佳实践
6.1 模板 Ref
通过 ref 可以直接访问子组件或 DOM 元素。
<template>
<ChildComponent ref="childRef" />
<div ref="domRef">DOM Element</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const childRef = ref(null)
const domRef = ref(null)
onMounted(() => {
// 访问子组件方法
childRef.value.someMethod()
// 访问 DOM
console.log(domRef.value.textContent)
})
</script>6.2 依赖注入
当组件层级较深时,使用 provide/inject 比 props 层层传递更优雅。
// 提供令牌和更新方法
provide('formKey', Symbol('form'))
provide('registerField', (field) => {
// 注册字段逻辑
})
provide('unregisterField', (field) => {
// 注销字段逻辑
})6.3 Slot 通信
通过 slot 可以实现父组件向子组件传递UI结构,实现定制化展示。
<!-- DataList.vue -->
<template>
<div class="list">
<slot
v-for="item in items"
:item="item"
:index="items.indexOf(item)"
/>
</div>
</template>
<script setup>
defineProps({
items: Array
})
</script><!-- 父组件使用 -->
<DataList :items="dataItems">
<template #default="{ item, index }">
<div class="item">
{{ index }} - {{ item.name }}
</div>
</template>
</DataList>7. 原理分析:响应式系统的依赖追踪
Vue 的响应式系统基于依赖追踪(Dependency Tracking)实现,当数据变化时自动更新相关视图。
7.1 响应式原理
// 简化版的响应式系统
class Reactive {
constructor(data) {
this.data = data
this.subscribers = new Set()
return new Proxy(data, {
get(target, key) {
// 收集依赖
if (activeEffect) {
this.subscribers.add(activeEffect)
}
return target[key]
},
set(target, key, value) {
target[key] = value
// 触发更新
this.subscribers.forEach(effect => effect())
return true
}
})
}
}
let activeEffect = null
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}7.2 组件更新的依赖追踪
// Watcher 类简化实现
class Watcher {
constructor(component, getter) {
this.component = component
this.getter = getter
this.value = this.get()
}
get() {
// 将当前 watcher 设置为活跃
pushTarget(this)
// 执行 getter,触发响应式数据的 get
const value = this.getter.call(this.component)
// 恢复
popTarget()
return value
}
update() {
this.value = this.get()
// 触发组件重新渲染
this.component.$forceUpdate()
}
}7.3 关键流程
- 组件挂载时:创建 Watcher,收集组件模板中使用的所有响应式依赖
- 数据变化时:响应式 setter 被触发,通知所有依赖该数据的 Watcher
- Watcher 更新:调用组件的更新方法,重新渲染组件
// Vue 2 中 $forceUpdate 的原理
Vue.component('Comp', {
methods: {
update() {
// 直接调用 watcher 的 update 方法
this._watcher.update()
}
}
})8. 总结
| 通信方式 | 适用场景 | Vue 2 | Vue 3 |
|---|---|---|---|
| props/emits | 父子通信 | ✓ | ✓ |
| parent/children | 父子通信(不推荐) | ✓ | ✗ |
| provide/inject | 跨级通信 | ✓ | ✓ |
| event bus | 任意组件通信 | ✓ | ✗ |
| mitt | 任意组件通信 | ✗ | ✓ |
| Pinia/Vuex | 全局状态管理 | ✗/✓ | ✓ |
| ref | 访问子组件/DOM | ✓ | ✓ |
| slot | 内容分发 | ✓ | ✓ |
选择合适的通信方式需要根据组件关系层级、复杂度、维护成本等因素综合考虑。
