Vue组件通信全场景详解(Vue2+Vue3适配)| 实战必备,新手也能看懂

23 阅读7分钟

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. 跨级组件通信:attrs/attrs / 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 / refs,批量操作子用refs**,批量操作子用 **children,子访问父用 $parent,传递内容用 插槽
  • 兄弟组件:中小型项目用 EventBus,大型项目用 Pinia
  • 跨级组件:简单共享用 Provide/Injectroot,需传递props/事件用root**,需传递props/事件用 **attrs / useAttrs,复杂共享用 Pinia
  • 页面级组件:用 路由传参
  • 全局复杂状态:大型项目用 Pinia / Vuex,小型项目用 $rootProvide/Inject

总结:Vue 组件通信的核心是“数据流转清晰”,优先使用原生支持的方式,根据组件关系和项目规模选择合适的方案,大型项目统一用 Pinia 管理全局状态,避免出现“通信混乱”的问题,提升代码可维护性。