Vue3 组件通信全攻略:全部方式与实战示例

在Vue3中,组件通信是构建应用的核心技能。本文系统梳理多种通信方式,从基础到进阶,结合真实场景与代码示例,帮助开发者灵活选择最佳方案。

一、父子组件通信

1. Props + Emit(基础单向数据流)

场景:父组件传递数据给子组件,子组件触发事件通知父组件
示例

// Parent.vue
<template>
  <Child :message="parentMsg" @update="handleUpdate"/>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const parentMsg = ref('Hello from Parent')
const handleUpdate = (newMsg) => {
  parentMsg.value = newMsg
}
</script>

// Child.vue
<template>
  <div>{{ message }} <button @click="sendMessage">Send</button></div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'

const props = defineProps({
  message: String
})
const emit = defineEmits(['update'])

const sendMessage = () => {
  emit('update', 'Hello from Child')
}
</script>

2. $attrs(透传未声明的属性)

场景:子组件需要接收父组件传递的非Props属性
示例

// Parent.vue
<template>
  <Child :msg="message" :other="extraData" />
</template>
<script setup>
import Child from './Child.vue'

const message = 'Main Message'
const extraData = { id: 123 }
</script>

// Child.vue
<template>
  <div v-bind="$attrs"></div>
</template>
<script setup>
// $attrs自动包含父组件传递的other属性
</script>

3. Ref + DefineExpose(父直接调用子组件方法)

场景:父组件需要直接访问子组件的属性或方法
示例

// Parent.vue
<template>
  <Child ref="childRef" />
  <button @click="callChildMethod">调用子组件方法</button>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const childRef = ref(null)
const callChildMethod = () => {
  childRef.value?.childFunction()
}
</script>

// Child.vue
<template>
  <div>子组件内容</div>
</template>
<script setup>
import { defineExpose } from 'vue'

const childFunction = () => {
  console.log('父组件调用了子组件方法')
}

// 显式暴露子组件方法(Vue3 setup默认私有)
defineExpose({ childFunction })
</script>

二、兄弟组件通信

1. 父组件中转(基础方式)

场景:两个同级子组件之间传递数据,通过共同父组件作为中间层
示例

// Parent.vue
<template>
  <BrotherA @sendData="handleDataFromA" />
  <BrotherB :data="brotherData" />
</template>
<script setup>
import { ref } from 'vue'
import BrotherA from './BrotherA.vue'
import BrotherB from './BrotherB.vue'

const brotherData = ref('')
const handleDataFromA = (data) => {
  brotherData.value = data
}
</script>

// BrotherA.vue(发送方)
<template>
  <button @click="sendToBrother">给B传数据</button>
</template>
<script setup>
import { defineEmits } from 'vue'

const emit = defineEmits(['sendData'])
const sendToBrother = () => {
  emit('sendData', '来自A的数据')
}
</script>

// BrotherB.vue(接收方)
<template>
  <div>收到A的数据:{{ data }}</div>
</template>
<script setup>
import { defineProps } from 'vue'

const props = defineProps({
  data: String
})
</script>

2. EventBus(mitt库实现)

场景:多个非父子组件之间的轻量通信(Vue3移除内置EventBus,需用第三方库)
示例

// 1. 安装mitt:npm install mitt
// 2. 创建eventBus.js
import mitt from 'mitt'
export const eventBus = mitt()

// 3. 发送方组件
<script setup>
import { eventBus } from './eventBus.js'

const sendMsg = () => {
  eventBus.emit('msg-event', 'EventBus传递的消息')
}
</script>

// 4. 接收方组件
<script setup>
import { eventBus } from './eventBus.js'
import { onMounted, onUnmounted } from 'vue'

onMounted(() => {
  // 监听事件
  eventBus.on('msg-event', (msg) => {
    console.log('收到消息:', msg)
  })
})

onUnmounted(() => {
  // 销毁事件(避免内存泄漏)
  eventBus.off('msg-event')
})
</script>

三、跨层级组件通信

1. Provide / Inject(依赖注入)

场景:多层级组件(如爷孙)之间传递数据,无需逐层透传Props
示例

// 顶层组件(Provider)
<script setup>
import { provide, ref } from 'vue'

const globalTheme = ref('light')
// 提供数据(key: 标识,value: 数据)
provide('theme-key', {
  theme: globalTheme,
  changeTheme: (newTheme) => {
    globalTheme.value = newTheme
  }
})
</script>

// 深层子组件(Injector)
<script setup>
import { inject } from 'vue'

// 注入数据(通过相同的key)
const themeConfig = inject('theme-key')
// 使用数据
console.log(themeConfig.theme.value) // light
// 调用方法
const switchTheme = () => {
  themeConfig.changeTheme('dark')
}
</script>

2. Pinia(Vue3官方状态管理)

场景:大型应用中多组件共享状态(替代Vuex)
示例

// 1. 安装Pinia:npm install pinia
// 2. 创建store(stores/counter.js)
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++
    }
  },
  getters: {
    doubleCount: (state) => state.count * 2
  }
})

// 3. 全局注册Pinia(main.js)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

// 4. 任意组件中使用
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const counterStore = useCounterStore()
// 解构state(保持响应式)
const { count, doubleCount } = storeToRefs(counterStore)
// 调用action
const addCount = () => {
  counterStore.increment()
}
</script>

四、其他进阶通信方式

1. Teleport + 状态共享

场景:Teleport组件(传送门)与原组件之间的通信
示例

// 原组件
<template>
  <button @click="openModal">打开弹窗</button>
  <Teleport to="body">
    <Modal :visible="isModalVisible" @close="isModalVisible = false" />
  </Teleport>
</template>
<script setup>
import { ref } from 'vue'
import Modal from './Modal.vue'

const isModalVisible = ref(false)
const openModal = () => {
  isModalVisible.value = true
}
</script>

// Modal.vue(Teleport目标组件)
<template>
  <div v-if="visible" class="modal">
    <div>弹窗内容</div>
    <button @click="emit('close')">关闭</button>
  </div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'

const props = defineProps({
  visible: Boolean
})
const emit = defineEmits(['close'])
</script>

2. Vue Router(路由参数/Query)

场景:不同路由页面之间传递数据
示例

// 跳转时传参(方式1:路由参数)
<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()
const goToDetail = (id) => {
  router.push({
    path: `/detail/${id}`, // 需在路由配置中定义:path: '/detail/:id'
  })
}
</script>

// 接收路由参数
<script setup>
import { useRoute } from 'vue-router'

const route = useRoute()
// 响应式获取参数
const detailId = route.params.id
</script>

// 跳转时传参(方式2:Query参数)
<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()
const goToSearch = (keyword) => {
  router.push({
    path: '/search',
    query: { keyword }
  })
}
</script>

// 接收Query参数
<script setup>
import { useRoute } from 'vue-router'

const route = useRoute()
const searchKeyword = route.query.keyword
</script>

3. Slots(作用域插槽传值)

场景:父组件通过插槽向子组件传递内容,同时子组件向父组件传递数据
示例

// 子组件(提供插槽数据)
<template>
  <div class="list">
    <slot v-for="item in list" :item="item" :index="index">
      <!-- 默认内容 -->
      <div>{{ item.name }}</div>
    </slot>
  </div>
</template>
<script setup>
import { ref } from 'vue'

const list = ref([
  { id: 1, name: '项目1' },
  { id: 2, name: '项目2' }
])
</script>

// 父组件(使用作用域插槽)
<template>
  <ListComponent>
    <template #default="{ item, index }">
      <div>第{{ index + 1 }}项:{{ item.name }}(ID:{{ item.id }})</div>
    </template>
  </ListComponent>
</template>
<script setup>
import ListComponent from './ListComponent.vue'
</script>

4. v-model(组件双向绑定)

场景:父组件与子组件之间的双向数据同步(语法糖)
示例

// 子组件
<template>
  <input 
    type="text" 
    :value="modelValue" 
    @input="emit('update:modelValue', $event.target.value)"
  >
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'

// v-model默认绑定modelValue
const props = defineProps({
  modelValue: String
})
const emit = defineEmits(['update:modelValue'])
</script>

// 父组件使用
<template>
  <CustomInput v-model="inputValue" />
  <div>输入内容:{{ inputValue }}</div>
</template>
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'

const inputValue = ref('')
</script>

5. Composition API 工具函数(抽离通信逻辑)

场景:多个组件复用相同的通信逻辑(如接口请求+状态共享)
示例

// 抽离的工具函数(composables/useUser.js)
import { ref, onMounted } from 'vue'
import { getUserInfo } from '@/api/user'

export const useUser = () => {
  const userInfo = ref(null)
  const loading = ref(false)

  const fetchUser = async () => {
    loading.value = true
    try {
      const res = await getUserInfo()
      userInfo.value = res.data
    } catch (err) {
      console.error('获取用户信息失败', err)
    } finally {
      loading.value = false
    }
  }

  onMounted(fetchUser)

  return { userInfo, loading, fetchUser }
}

// 任意组件中使用
<script setup>
import { useUser } from '@/composables/useUser'

const { userInfo, loading, fetchUser } = useUser()
</script>

6. Pinia 高级用法(Action跨组件调用)

场景:一个Pinia Store的Action调用另一个Store的方法,实现跨组件逻辑联动
示例

// stores/user.js
import { defineStore } from 'pinia'
import { useCartStore } from './cart'

export const useUserStore = defineStore('user', {
  state: () => ({
    token: null
  }),
  actions: {
    async login(loginForm) {
      // 登录接口请求
      const res = await loginApi(loginForm)
      this.token = res.data.token
      
      // 调用购物车Store的方法,同步用户购物车
      const cartStore = useCartStore()
      await cartStore.syncCart()
    }
  }
})

// stores/cart.js
import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    cartList: []
  }),
  actions: {
    async syncCart() {
      // 根据用户token同步购物车
      const res = await getCartListApi()
      this.cartList = res.data
    }
  }
})

总结:通信方式选择指南

通信场景推荐方式
父子组件基础传值Props + Emit
父子组件方法调用Ref + DefineExpose
父子透传非Props属性$attrs
兄弟组件通信父组件中转 / EventBus(mitt)
多层级组件通信Provide/Inject / Pinia
大型应用全局状态共享Pinia
路由页面间传值Vue Router(参数/Query)
插槽内容与数据联动作用域插槽
组件双向绑定v-model
通信逻辑复用Composition API 工具函数

其他

四下皆无人