Vue 组件通信是前端开发核心知识点,不同组件层级(父子、兄弟、跨级)对应不同的通信方案,核心原则是:简洁、高效、贴合场景,避免过度复杂的通信逻辑。以下按「基础场景→进阶场景→特殊场景」梳理,结合实战常用方式,覆盖 Vue2、Vue3 通用用法,附实战示例,直接适配项目开发。
一、基础场景:父子组件通信(最常用)
父子组件是最常见的组件关系(如父组件嵌套子组件),通信方式简单直接,Vue 原生支持,无需额外依赖,以下涵盖所有父子通信相关方式。
1. 父传子:Props
核心:父组件通过自定义属性向子组件传递数据,子组件通过 props 接收,单向数据流(父变子变,子不能直接修改 props),是父子通信最基础、最常用的方式,耳熟能详,无需过多冗余说明。
// 父组件 Parent.vue
<template>
<!-- 传递普通值、对象、方法 -->
<Child
:name="userName"
:user="userInfo"
:handleClick="parentClick"
/>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const userName = ref('张三')
const userInfo = ref({ age: 22, role: 'admin' })
const parentClick = () => {
console.log('父组件方法被调用')
}
</script>
// 子组件 Child.vue
<template>
<div>{{ name }} - {{ user.age }}</div>
<button @click="handleClick">调用父组件方法</button>
</template>
<script setup>
// 接收父组件传递的数据,可指定类型、默认值、校验
const props = defineProps({
name: {
type: String,
required: true, // 必传
default: '未知用户'
},
user: {
type: Object,
default: () => ({}) // 对象默认值需用函数
},
handleClick: {
type: Function
}
})
</script>
注意:子组件若需修改 props,需通过「子传父」通知父组件修改,避免直接修改 props 破坏单向数据流。
2. 父传子:$refs
核心:若用在普通 DOM 元素上,引用指向该 DOM 元素;若用在子组件上,引用指向子组件实例,父组件可通过 $refs.xx 主动获取子组件的属性、调用子组件的方法,灵活高效。
// 父组件 Parent.vue
<template>
<Child ref="childRef" />
<button @click="operateChild">操作子组件</button>
<div ref="domRef">普通DOM元素</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const childRef = ref(null) // 绑定子组件ref
const domRef = ref(null) // 绑定普通DOM元素
onMounted(() => {
// 访问普通DOM元素
console.log(domRef.value.textContent) // 输出:普通DOM元素
})
const operateChild = () => {
// 访问子组件数据
console.log(childRef.value.childName)
// 调用子组件方法
childRef.value.childMethod()
}
</script>
// 子组件 Child.vue
<script setup>
import { ref } from 'vue'
const childName = ref('子组件')
const childMethod = () => {
console.log('子组件方法被调用')
}
// Vue3 setup 需主动暴露,父组件才能访问
defineExpose({
childName,
childMethod
})
</script>
3. 父传子:$children
核心:父组件通过 $children 获取一个包含所有直接子组件(不包含孙子组件)的 VueComponent 对象数组,可直接访问子组件中所有数据和方法,适合批量操作子组件的场景。
// 父组件 Parent.vue
<template>
<Child1 />
<Child2 />
<button @click="operateAllChildren">操作所有子组件</button>
</template>
<script>
// Vue2 用法(Vue3 setup 中需通过 getCurrentInstance 获取)
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
export default {
components: { Child1, Child2 },
methods: {
operateAllChildren() {
// 获取所有直接子组件,遍历操作
this.$children.forEach(child => {
console.log(child.childName) // 访问子组件数据
child.childMethod() // 调用子组件方法
})
}
}
}
// Vue3 setup 用法
import { getCurrentInstance } from 'vue'
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'
const instance = getCurrentInstance()
const operateAllChildren = () => {
instance.refs.$children.forEach(child => {
console.log(child.childName)
child.childMethod()
})
}
</script>
4. 父子双向绑定:.sync 修饰符
核心:可实现父组件向子组件传递数据的双向绑定,子组件接收到数据后可直接修改,且会同步修改父组件中的数据,简化双向通信代码,与 v-model 功能类似但适用场景更灵活。
// 父组件 Parent.vue
<template>
<!-- .sync 修饰符实现双向绑定 -->
<Child :count.sync="parentCount" />
<p>父组件count:{{ parentCount }}</p>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const parentCount = ref(0)
</script>
// 子组件 Child.vue
<template>
<button @click="changeCount">修改count(.sync)</button>
</template>
<script setup>
const props = defineProps(['count'])
const emit = defineEmits(['update:count'])
const changeCount = () => {
// 触发 update:属性名 事件,实现双向同步
emit('update:count', props.count + 1)
}
</script>
5. 父子双向绑定:v-model
核心:和 .sync 类似,可实现父组件传给子组件的数据双向绑定,本质是 props + emit 的语法糖,子组件通过 emit 修改父组件的数据,更适合表单类、开关类组件场景。
// 子组件 Child.vue(表单组件)
<template>
<input
type="text"
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
// v-model 默认绑定 modelValue,触发 update:modelValue 事件
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
// 父组件 Parent.vue
<template>
<!-- 双向绑定,无需额外监听事件 -->
<Child v-model="inputValue" />
<p>父组件接收值:{{ inputValue }}</p>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const inputValue = ref('')
</script>
6. 父传子:插槽(默认插槽、具名插槽)
核心:父组件通过插槽向子组件传递 HTML 模板、组件,实现“内容分发”,本质是父向子的通信,适合组件封装(如弹窗、卡片),分为默认插槽和具名插槽,满足不同内容分发需求。
// 子组件 Child.vue(插槽组件)
<template>
<div class="card">
<!-- 匿名插槽:接收父组件传递的任意内容,无名称 -->
<slot>默认内容(父组件未传内容时显示)</slot>
<!-- 具名插槽:接收指定名称的内容,实现多区域分发 -->
<slot name="header">默认头部</slot>
<slot name="footer">默认底部</slot>
</div>
</template>
// 父组件 Parent.vue
<template>
<Child>
<!-- 匿名插槽内容 -->
<p>卡片主体内容</p>
<!-- 具名插槽内容,通过 # 简写指定插槽名称 -->
<template #header>
<h3>卡片标题</h3>
</template>
<template #footer>
<button>点击按钮</button>
</template>
</Child>
</template>
7. 子传父:$emit / v-on
核心:子组件通过派发自定义事件的方式,向父组件传递数据,或触发父组件的更新等操作,父组件通过 v-on 监听事件接收数据,是子传父最核心、最常用的方式。
// 子组件 Child.vue
<template>
<button @click="sendData">向父组件传值</button>
</template>
<script setup>
// 定义可触发的自定义事件
const emit = defineEmits(['sendMsg', 'updateName'])
const sendData = () => {
// 触发事件,可传递1个或多个参数
emit('sendMsg', '子组件传递的消息', 123)
emit('updateName', '李四') // 通知父组件修改名称
}
</script>
// 父组件 Parent.vue
<template>
<Child
@sendMsg="getMsg"
@updateName="userName = $event"
/>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const userName = ref('张三')
const getMsg = (msg, num) => {
console.log('接收子组件消息:', msg, num) // 输出:子组件传递的消息 123
}
</script>
8. 子传父:$parent
核心:子组件通过 $parent 获取父节点的 VueComponent 对象,包含父节点中所有数据和方法,可直接访问父组件的属性、调用父组件的方法,实现子向父的通信。
// 父组件 Parent.vue
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const parentName = ref('父组件')
const parentMethod = () => {
console.log('父组件方法被调用')
}
// Vue3 setup 需暴露给子组件
defineExpose({
parentName,
parentMethod
})
</script>
// 子组件 Child.vue
<template>
<button @click="accessParent">访问父组件</button>
</template>
<script setup>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
const accessParent = () => {
// 访问父组件数据
console.log(instance.parent.parentName)
// 调用父组件方法
instance.parent.parentMethod()
}
</script>
9. 子传父:插槽(作用域插槽)
核心:作用域插槽是子传父的特殊形式,子组件通过插槽向父组件传递数据,父组件接收数据后,可根据数据自定义插槽内容,实现“子传数据、父定义渲染”的灵活通信。
// 子组件 Child.vue(作用域插槽)
<template>
<div>
<!-- 子组件向父组件传递数据(row、index) -->
<slot :row="user" :index="1">
默认内容(父组件未自定义插槽时显示)
</slot>
</div>
</template>
<script setup>
import { ref } from 'vue'
const user = ref({ name: '张三', age: 22 })
</script>
// 父组件 Parent.vue
<template>
<Child>
<!-- 父组件接收子组件传递的数据,自定义渲染内容 -->
<template #default="slotProps">
<p>姓名:{{ slotProps.row.name }}</p>
<p>索引:{{ slotProps.index }}</p>
</template>
</Child>
</template>
二、进阶场景:兄弟组件/跨级组件通信
当组件层级超过2层(如爷爷→父→子),或无直接关系(兄弟组件),使用父子通信会导致代码冗余,需用进阶方案,以下补充完善跨级、兄弟通信的所有常用方式。
1. 兄弟组件通信:事件总线(EventBus)
核心:通过一个空的 Vue 实例作为中央事件总线(事件中心),不管是父子组件、兄弟组件、跨层级组件等,都可以通过它完成通信操作,适合中小型项目。
// 1. 创建事件总线(utils/eventBus.js)
import { ref, onUnmounted } from 'vue'
export const useEventBus = () => {
const events = ref({}) // 存储事件订阅
// 订阅事件
const on = (eventName, callback) => {
if (!events.value[eventName]) {
events.value[eventName] = []
}
events.value[eventName].push(callback)
}
// 发布事件
const emit = (eventName, ...args) => {
if (events.value[eventName]) {
events.value[eventName].forEach(callback => callback(...args))
}
}
// 取消订阅(避免内存泄漏)
const off = (eventName, callback) => {
if (events.value[eventName]) {
events.value[eventName] = events.value[eventName].filter(cb => cb !== callback)
}
}
// 组件卸载时自动取消订阅
const useAutoOff = (eventName, callback) => {
on(eventName, callback)
onUnmounted(() => off(eventName, callback))
}
return { on, emit, off, useAutoOff }
}
// 2. 兄弟组件 A(发布事件)
<script setup>
import { useEventBus } from '@/utils/eventBus'
const { emit } = useEventBus()
const sendToBrother = () => {
emit('brotherMsg', '来自兄弟A的消息')
}
</script>
// 3. 兄弟组件 B(订阅事件)
<script setup>
import { useEventBus } from '@/utils/eventBus'
const { useAutoOff } = useEventBus()
// 自动取消订阅,避免内存泄漏
useAutoOff('brotherMsg', (msg) => {
console.log('接收兄弟A消息:', msg)
})
</script>
2. 跨级组件通信:listeners(Vue2)/ useAttrs(Vue3)
核心:$attrs 包含了父作用域中未被 prop 所识别且获取的特性绑定(class 和 style 除外),可通过 v-bind="$attrs" 传入内部组件;$listeners 包含了父作用域中的(不含 .native 修饰器的)v-on 事件监听器,可通过 v-on="$listeners" 传入内部组件,无需手动层层传递。
// Vue3 用法(useAttrs)
// 爷爷组件
<template>
<Parent :name="userName" :age="22" @click="parentClick" @updateName="updateName" />
</template>
<script setup>
import { ref } from 'vue'
import Parent from './Parent.vue'
const userName = ref('张三')
const parentClick = () => console.log('爷爷组件点击事件')
const updateName = (newName) => userName.value = newName
</script>
// 父组件(无需接收props,直接传递给子组件)
<template>
<Child v-bind="attrs" v-on="listeners" />
</template>
<script setup>
import { useAttrs, useListeners } from 'vue'
const attrs = useAttrs() // 接收所有未被prop识别的属性
const listeners = useListeners() // 接收所有父组件事件
</script>
// 子组件(直接接收爷爷组件传递的内容)
<script setup>
const props = defineProps(['name', 'age'])
const emit = defineEmits(['click', 'updateName'])
// 触发爷爷组件的事件
const triggerGrandpaEvent = () => {
emit('click')
emit('updateName', '李四')
}
</script>
3. 跨级组件通信:Provide / Inject(依赖注入)
核心:祖先组件中通过 provide 来提供变量,然后在子孙组件中通过 inject 来注入变量,跨级组件间建立了一种主动提供与依赖注入的关系,适合跨级共享简单数据(如主题、用户信息)。
// 祖先组件(顶层组件,如 App.vue)
<script setup>
import { provide, ref } from 'vue'
// 提供数据(可提供响应式数据)
const theme = ref('light')
provide('theme', theme)
// 提供方法(修改共享数据)
const changeTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide('changeTheme', changeTheme)
</script>
// 子组件(任意层级,无需通过父组件传递)
<script setup>
import { inject } from 'vue'
// 接收共享数据和方法
const theme = inject('theme')
const changeTheme = inject('changeTheme')
</script>
<template>
<div :class="theme">当前主题:{{ theme }}</div>
<button @click="changeTheme">切换主题</button>
</template>
注意:Provide/Inject 是“单向向下”的通信,子组件不能直接修改注入的数据,需通过祖先组件提供的方法修改,保证数据统一。
4. 跨级组件通信:$root
核心:通过 $root 可直接拿到根组件(App.vue)里的数据和方法,所有组件都能通过 $root 访问根组件的内容,适合简单的全局共享场景(无需复杂状态管理)。
// 根组件 App.vue
<script setup>
import { ref } from 'vue'
const globalMsg = ref('全局共享消息')
const globalMethod = () => {
console.log('根组件全局方法被调用')
}
// 暴露给所有子组件
defineExpose({
globalMsg,
globalMethod
})
</script>
// 任意子组件(无论层级)
<script setup>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
const accessRoot = () => {
// 访问根组件数据
console.log(instance.root.globalMsg)
// 调用根组件方法
instance.root.globalMethod()
}
</script>
5. 全局状态管理:Pinia / Vuex
核心:状态管理器,集中式存储管理所有组件的状态,任意组件(无论层级、关系)都可直接读写,适合大型项目、多组件共享数据(如用户登录状态、购物车、全局配置)。Vue2 项目使用 Vuex,Vue3 优先推荐 Pinia(比 Vuex 简洁,无需 mutations)。
// 1. 安装 Pinia 并创建仓库(store/user.js)
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: null, // 全局共享的用户信息
token: ''
}),
actions: {
// 修改状态的方法
setUserInfo(info) {
this.userInfo = info
this.token = info.token
},
logout() {
this.userInfo = null
this.token = ''
}
}
})
// 2. 任意组件使用(读取/修改状态)
<script setup>
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
// 读取状态
console.log(userStore.userInfo)
// 修改状态(通过 actions,避免直接修改 state)
userStore.setUserInfo({ name: '张三', token: 'abc123' })
// 直接修改状态(简单场景,不推荐复杂项目)
userStore.token = 'def456'
</script>
// Vue2 Vuex 简单示例
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: { userInfo: null },
mutations: {
setUserInfo(state, info) {
state.userInfo = info
}
},
actions: {
setUserInfo({ commit }, info) {
commit('setUserInfo', info)
}
}
})
// 组件中使用
this.$store.dispatch('setUserInfo', { name: '张三' })
console.log(this.$store.state.userInfo)
三、特殊场景:其他通信方式
针对特定场景(如页面级组件通信),使用以下方式更高效,避免过度使用全局状态。
1. 路由传参(组件无直接关系,通过路由跳转通信)
核心:通过路由参数传递数据,适合页面级组件通信(如列表页→详情页),分为「query 参数」和「params 参数」,无需组件间建立直接关联。
// 1. 跳转时传递参数(列表页)
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const goToDetail = (id) => {
// 方式1:query 参数(暴露在URL上,可刷新,适合简单数据)
router.push({
path: '/detail',
query: { id, name: '张三' }
})
// 方式2:params 参数(不暴露在URL上,刷新丢失,适合敏感数据)
router.push({
name: 'Detail', // 需配置路由name
params: { id, name: '张三' }
})
}
</script>
// 2. 详情页接收参数
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
// 接收 query 参数
console.log(route.query.id)
// 接收 params 参数
console.log(route.params.id)
</script>
四、通信方式选型建议(实战重点)
开发中无需死记所有方式,根据组件关系和项目规模选择,避免过度复杂。以下为各通信方式适用场景对比表,可快速查阅选型:
| 通信方式 | 适用组件关系 | 适用项目规模 | 核心优势 | 注意事项 |
|---|---|---|---|---|
| Props + emit | 父子组件 | 所有规模 | 原生支持、简洁直接、单向数据流清晰,最常用 | 子组件不能直接修改props,需通过emit通知父组件 |
| v-model | 父子组件(表单/开关类) | 所有规模 | 简化双向绑定代码,语法简洁,适配表单场景 | 默认绑定modelValue,需配合update:modelValue事件 |
| .sync 修饰符 | 父子组件 | 所有规模 | 实现双向绑定,比v-model更灵活,无需固定属性名 | 需触发update:属性名事件,才能同步修改父组件数据 |
| ref / $refs | 父子组件 | 所有规模 | 父组件可直接操作子组件实例或DOM,灵活高效 | Vue3 setup需用defineExpose暴露属性/方法 |
| $children | 父子组件(父→多个直接子组件) | 所有规模 | 可批量访问所有直接子组件,适合批量操作 | 不包含孙子组件,Vue3 setup需通过getCurrentInstance获取 |
| $parent | 父子组件(子→父) | 所有规模 | 子组件可直接访问父组件实例,无需emit传递 | Vue3 setup需通过getCurrentInstance获取父组件 |
| 插槽(默认/具名) | 父子组件(内容分发) | 所有规模 | 可传递模板/组件,适合组件封装,灵活度高 | 仅用于传递内容,不适合传递复杂数据 |
| 插槽(作用域) | 父子组件(子传父数据) | 所有规模 | 子传数据、父定义渲染,兼顾灵活性和数据传递 | 需通过插槽props接收子组件传递的数据 |
| EventBus(事件总线) | 兄弟组件、跨级组件 | 中小型项目 | 实现简单,无需层层传递,适配各类无直接关系组件 | 需手动取消订阅,避免内存泄漏,大型项目易混乱 |
| Provide / Inject | 跨级组件(祖先→子孙) | 所有规模(简单共享) | 跨级传递无需中间组件转发,简洁高效 | 单向向下通信,子组件不能直接修改注入数据 |
| $attrs / useAttrs | 跨级组件 | 所有规模 | 无需手动层层传递props和事件,减少冗余 | 不适合传递大量复杂数据,易造成数据混乱 |
| $root | 任意组件(访问根组件) | 中小型项目(简单全局共享) | 直接访问根组件数据/方法,实现简单 | 不适合复杂全局状态,大型项目易造成数据混乱 |
| Pinia / Vuex | 任意组件(全局共享) | 大型项目 | 统一管理全局状态,数据流转可追溯,适配复杂场景 | 小型项目使用会增加冗余,Vue3优先Pinia |
| 路由传参 | 页面级组件(无直接关系) | 所有规模 | 适合页面跳转时传递数据,无需组件关联 | params参数刷新丢失,query参数暴露在URL |
- 父子组件:优先用 Props + emit,双向绑定用 v-model 或 .sync 修饰符,父操作子用 ref / children,子访问父用 $parent,传递内容用 插槽;
- 兄弟组件:中小型项目用 EventBus,大型项目用 Pinia;
- 跨级组件:简单共享用 Provide/Inject 或 attrs / useAttrs,复杂共享用 Pinia;
- 页面级组件:用 路由传参;
- 全局复杂状态:大型项目用 Pinia / Vuex,小型项目用 $root 或 Provide/Inject。
总结:Vue 组件通信的核心是“数据流转清晰”,优先使用原生支持的方式,根据组件关系和项目规模选择合适的方案,大型项目统一用 Pinia 管理全局状态,避免出现“通信混乱”的问题,提升代码可维护性。