Vue组件之间通信

30 阅读4分钟

Vue 组件通信主要分为三大类:

  1. 父子组件通信 (最常见)
  2. 兄弟/跨级组件通信
  3. 全局状态管理

一、父子组件通信

1. 父传子:Props

这是最基础、最常用的方式。父组件通过属性(Attributes)向下传递数据,子组件通过 props 选项来声明接收。

子组件 (ChildComponent.vue):

<script setup>
// 1. 使用 defineProps 编译器宏来声明 props
const props = defineProps({
  title: String, // 基础类型检查
  userInfo: Object, // 复杂类型检查
  likes: {
    type: Number,
    default: 0 // 默认值
  },
  isPublished: Boolean
})

// 在脚本中使用 props
console.log(props.title)
</script>

<template>
  <div>
    <h2>{{ title }}</h2>
    <p>Likes: {{ likes }}</p>
  </div>
</template>

父组件 (ParentComponent.vue):

<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const postTitle = ref('Hello Vue!')
const author = ref({ name: 'Alice', age: 25 })
const count = ref(10)
</script>

<template>
  <!-- 2. 通过属性传递数据 -->
  <ChildComponent 
    :title="postTitle" 
    :user-info="author" 
    :likes="count" 
    is-published 
  />
</template>

2. 子传父:自定义事件 (Emits)

子组件通过触发(Emit)自定义事件,向父组件传递数据。

子组件 (ChildComponent.vue):

<script setup>
// 1. 声明它可能触发的自定义事件
const emit = defineEmits(['enlarge-text', 'submit'])

function handleClick() {
  // 2. 触发事件,并传递数据
  emit('enlarge-text', 2) // 传递一个数字
  // emit('submit', { username: 'alice', password: 'secret' }) // 传递一个对象
}
</script>

<template>
  <button @click="handleClick">Enlarge Text</button>
  <!-- 也可以直接在模板中触发 -->
  <!-- <button @click="$emit('enlarge-text', 2)">Enlarge Text</button> -->
</template>

父组件 (ParentComponent.vue):

<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const fontSize = ref(1)
const textSize = ref(1)

// 3. 监听子组件触发的事件
function onEnlargeText(scale) {
  fontSize.value += scale
}
</script>

<template>
  <div :style="{ fontSize: fontSize + 'em' }">
    <!-- 4. 使用 v-on 或 @ 来监听子组件发出的事件 -->
    <ChildComponent @enlarge-text="onEnlargeText" />
    <!-- 或者使用内联方式 -->
    <ChildComponent @enlarge-text="textSize += $event" />
  </div>
</template>

3. 父组件直接访问子组件:模板引用 (Template Refs)

父组件通过 ref 属性获取子组件实例或 DOM 元素,从而直接调用其方法或访问其数据。

子组件 (ChildComponent.vue):

<script setup>
import { ref } from 'vue'

const input = ref(null)
const message = ref('Hello from Child!')

// 暴露给父组件的方法或属性
defineExpose({
  message,
  focusInput: () => {
    input.value?.focus()
  }
})
</script>

<template>
  <input ref="input" />
</template>

父组件 (ParentComponent.vue):

<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'

// 1. 创建一个同名的 ref
const childRef = ref(null)

onMounted(() => {
  // 2. 子组件挂载后,可以通过 ref 访问
  console.log(childRef.value.message) // 'Hello from Child!'
  childRef.value.focusInput() // 调用子组件的方法
})
</script>

<template>
  <!-- 3. 在子组件上绑定 ref -->
  <ChildComponent ref="childRef" />
</template>

二、兄弟/跨级组件通信

当组件层级关系很深时,使用 Props 逐层传递会非常繁琐(称为“Prop 逐级透传”问题)。这时有以下解决方案:

1. Provide / Inject (提供/注入)

祖先组件提供(Provide)数据,任意后代组件都可以注入(Inject)使用,无需通过中间每一层传递。

祖先组件 (Ancestor.vue):

<script setup>
import { provide, ref } from 'vue'
import ParentComponent from './ParentComponent.vue'

const theme = ref('dark')

// 提供静态数据
provide('app-theme', 'dark') // key: 字符串,value: 任何值

// 提供响应式数据,使注入方能响应变化
provide('theme', theme) 

// 提供修改数据的方法
function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide('toggleTheme', toggleTheme)
</script>

<template>
  <ParentComponent />
</template>

后代组件 (Descendant.vue) - 可以是任意深层级的子组件:

<script setup>
import { inject } from 'vue'

// 注入数据,如果没有找到,则使用默认值 'light'
const theme = inject('theme', 'light')
// 注入方法
const toggleTheme = inject('toggleTheme')

// 注入非响应式数据
const appTheme = inject('app-theme')
</script>

<template>
  <button @click="toggleTheme">Toggle Theme (Current: {{ theme }})</button>
</template>

2. 全局事件总线 (Event Bus) - (Vue 3 较少使用)

创建一个全局的 Vue 实例来充当中央事件总线,任何组件都可以在上面触发和监听事件。

eventBus.js:

import mitt from 'mitt' // 一个轻量的第三方事件库

const emitter = mitt()
export default emitter

组件 A (触发事件):

<script setup>
import emitter from './eventBus'

function sendMessage() {
  emitter.emit('user-message', { text: 'Hello from Component A!' })
}
</script>

组件 B (监听事件):

<script setup>
import { onUnmounted } from 'vue'
import emitter from './eventBus'

// 监听事件
emitter.on('user-message', (message) => {
  console.log(message.text)
})

// 组件卸载时取消监听,防止内存泄漏
onUnmounted(() => {
  emitter.off('user-message')
})
</script>

注意:在大型项目中,事件总线容易导致事件流混乱,难以维护,通常更推荐使用 Pinia。


三、全局状态管理 (Pinia)

对于复杂的应用数据,多个不相关的组件需要共享状态时,使用全局状态管理库是最佳实践。Vue 官方推荐 Pinia(替代了旧的 Vuex)。

store/counter.js:

import { defineStore } from 'pinia'

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

任何组件中都可以使用:

<script setup>
import { useCounterStore } from '@/stores/counter'

// 在 setup 中访问 store
const counterStore = useCounterStore()
</script>

<template>
  <div>{{ counterStore.count }}</div>
  <div>{{ counterStore.doubleCount }}</div>
  <button @click="counterStore.increment()">Increment</button>
  <!-- 也可以使用 $patch 直接修改 -->
  <button @click="counterStore.$patch({ count: 10 })">Set to 10</button>
</template>

总结与选择建议

通信方式适用场景优点缺点
Props / Emits父子组件简单、直观、明确的数据流深层级组件传递麻烦
Template Refs父操作子需要直接调用子组件方法或访问 DOM 时破坏了组件的封装性,应谨慎使用
Provide/Inject跨多级组件避免 Prop 逐级透传,祖先和后代解耦数据流变得不明确,不利于调试
Event Bus任意组件简单快速,任意组件通信事件流难以追踪,大型项目中易混乱
Pinia (推荐)全局状态共享集中管理,响应式,DevTools 支持,类型安全需要学习额外的概念,对于简单场景稍重

简单选择指南:

  1. 父子通信:首选 Props 和 Emits
  2. 父需直接调用子:使用 Template Refs
  3. 爷爷传孙子(或更深) :使用 Provide / Inject
  4. 完全不相关的组件或复杂全局状态:使用 Pinia