组件通信 #

一、通信方式概览 #

text
┌─────────────────────────────────────────────────────┐
│                     父组件                           │
│  ┌─────────────────────────────────────────────┐   │
│  │ Props ↓              ↑ Emits                │   │
│  │           ┌─────────────────┐               │   │
│  │           │    子组件 A      │               │   │
│  │           └─────────────────┘               │   │
│  │                                              │   │
│  │  Provide ↓           ↑ Inject               │   │
│  │           ┌─────────────────┐               │   │
│  │           │    子组件 B      │               │   │
│  │           └─────────────────┘               │   │
│  └─────────────────────────────────────────────┘   │
│                                                     │
│  兄弟组件通信:Event Bus / Vuex / Pinia             │
└─────────────────────────────────────────────────────┘

二、Props(父传子) #

2.1 基本用法 #

vue
<!-- 父组件 -->
<template>
  <Child 
    title="标题" 
    :count="count"
    :user="user"
  />
</template>

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

const count = ref(0)
const user = ref({ name: '张三', age: 25 })
</script>
vue
<!-- 子组件 Child.vue -->
<template>
  <h2>{{ title }}</h2>
  <p>计数: {{ count }}</p>
  <p>用户: {{ user.name }}</p>
</template>

<script setup>
defineProps({
  title: String,
  count: Number,
  user: Object
})
</script>

2.2 Props验证 #

vue
<script setup>
defineProps({
  title: {
    type: String,
    required: true,
    validator(value) {
      return value.length > 0
    }
  },
  count: {
    type: Number,
    default: 0
  },
  type: {
    type: String,
    default: 'primary',
    validator(value) {
      return ['primary', 'success', 'warning', 'danger'].includes(value)
    }
  }
})
</script>

2.3 使用useAttrs获取未声明的Props #

vue
<template>
  <div v-bind="attrs">
    <slot></slot>
  </div>
</template>

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

const attrs = useAttrs()
// 父组件传递但未在defineProps中声明的属性
</script>

三、Emits(子传父) #

3.1 基本用法 #

vue
<!-- 子组件 Child.vue -->
<template>
  <button @click="handleClick">点击</button>
</template>

<script setup>
const emit = defineEmits(['update', 'delete'])

function handleClick() {
  emit('update', { id: 1, name: 'updated' })
}
</script>
vue
<!-- 父组件 -->
<template>
  <Child 
    @update="handleUpdate"
    @delete="handleDelete"
  />
</template>

<script setup>
import Child from './Child.vue'

function handleUpdate(data) {
  console.log('更新:', data)
}

function handleDelete(id) {
  console.log('删除:', id)
}
</script>

3.2 事件验证 #

vue
<script setup>
const emit = defineEmits({
  submit: (payload) => {
    if (!payload.email || !payload.password) {
      console.warn('无效的提交数据')
      return false
    }
    return true
  }
})

function handleSubmit() {
  emit('submit', { email: 'test@example.com', password: '123456' })
}
</script>

3.3 v-model语法糖 #

vue
<!-- 子组件 CustomInput.vue -->
<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
vue
<!-- 父组件 -->
<template>
  <CustomInput v-model="text" />
</template>

四、Provide/Inject(跨层级) #

4.1 基本用法 #

vue
<!-- 祖先组件 -->
<template>
  <Child />
</template>

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

const theme = ref('dark')
const user = ref({ name: '张三' })

provide('theme', theme)
provide('user', user)

function updateTheme(newTheme) {
  theme.value = newTheme
}

provide('updateTheme', updateTheme)
</script>
vue
<!-- 后代组件(任意层级) -->
<template>
  <div :class="theme">
    用户: {{ user.name }}
    <button @click="changeTheme">切换主题</button>
  </div>
</template>

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

const theme = inject('theme')
const user = inject('user')
const updateTheme = inject('updateTheme')

function changeTheme() {
  updateTheme(theme.value === 'dark' ? 'light' : 'dark')
}
</script>

4.2 默认值 #

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

// 提供默认值
const theme = inject('theme', 'light')

// 使用工厂函数作为默认值
const config = inject('config', () => ({
  apiUrl: '/api',
  timeout: 5000
}))
</script>

4.3 响应式Provide #

vue
<script setup>
import { provide, ref, readonly } from 'vue'

const state = ref({
  count: 0,
  user: null
})

// 提供只读状态
provide('state', readonly(state))

// 提供修改方法
provide('actions', {
  increment() {
    state.value.count++
  },
  setUser(user) {
    state.value.user = user
  }
})
</script>

4.4 使用Symbol作为key #

javascript
// keys.js
export const ThemeKey = Symbol('theme')
export const UserKey = Symbol('user')
vue
<script setup>
import { provide, inject } from 'vue'
import { ThemeKey, UserKey } from './keys'

// 提供
provide(ThemeKey, 'dark')

// 注入
const theme = inject(ThemeKey)
</script>

五、Event Bus(非父子通信) #

5.1 创建Event Bus #

javascript
// eventBus.js
import { ref } from 'vue'

class EventBus {
  constructor() {
    this.events = {}
  }
  
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = []
    }
    this.events[event].push(callback)
  }
  
  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(...args))
    }
  }
  
  off(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb => cb !== callback)
    }
  }
}

export const eventBus = new EventBus()

5.2 使用Event Bus #

vue
<!-- 组件A - 发送事件 -->
<script setup>
import { eventBus } from './eventBus'

function sendMessage() {
  eventBus.emit('message', 'Hello from A')
}
</script>
vue
<!-- 组件B - 接收事件 -->
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { eventBus } from './eventBus'

function handleMessage(msg) {
  console.log('收到消息:', msg)
}

onMounted(() => {
  eventBus.on('message', handleMessage)
})

onUnmounted(() => {
  eventBus.off('message', handleMessage)
})
</script>

5.3 使用mitt库(推荐) #

bash
npm install mitt
javascript
// eventBus.js
import mitt from 'mitt'
export const emitter = mitt()
vue
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { emitter } from './eventBus'

onMounted(() => {
  emitter.on('message', handleMessage)
})

onUnmounted(() => {
  emitter.off('message', handleMessage)
})

function handleMessage(msg) {
  console.log(msg)
}

function sendMessage() {
  emitter.emit('message', 'Hello')
}
</script>

六、$parent和$root #

6.1 访问父组件 #

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

const instance = getCurrentInstance()

function callParentMethod() {
  // 访问父组件
  instance.parent?.exposed?.someMethod()
}
</script>

6.2 访问根组件 #

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

const instance = getCurrentInstance()

function callRootMethod() {
  // 访问根组件
  instance.root?.exposed?.globalMethod()
}
</script>

七、组件通信最佳实践 #

7.1 选择指南 #

场景 推荐方式
父传子 Props
子传父 Emits
跨层级 Provide/Inject
兄弟组件 Pinia/Vuex
任意组件 Pinia/Vuex
轻量级兄弟通信 Event Bus

7.2 完整示例:用户管理 #

vue
<!-- 祖先组件 App.vue -->
<template>
  <div>
    <UserList />
    <UserDetail />
  </div>
</template>

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

const selectedUser = ref(null)
const users = ref([
  { id: 1, name: '张三', email: 'zhangsan@example.com' },
  { id: 2, name: '李四', email: 'lisi@example.com' }
])

provide('users', users)
provide('selectedUser', selectedUser)
provide('selectUser', (user) => {
  selectedUser.value = user
})
</script>
vue
<!-- UserList.vue -->
<template>
  <ul>
    <li v-for="user in users" :key="user.id" @click="selectUser(user)">
      {{ user.name }}
    </li>
  </ul>
</template>

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

const users = inject('users')
const selectUser = inject('selectUser')
</script>
vue
<!-- UserDetail.vue -->
<template>
  <div v-if="selectedUser">
    <h3>{{ selectedUser.name }}</h3>
    <p>{{ selectedUser.email }}</p>
  </div>
</template>

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

const selectedUser = inject('selectedUser')
</script>

八、总结 #

方式 方向 适用场景
Props 父→子 数据传递
Emits 子→父 事件通知
v-model 双向 表单组件
Provide/Inject 跨层级 深层嵌套
Event Bus 任意 轻量通信
Pinia/Vuex 全局 状态管理

通信原则:

  • 遵循单向数据流
  • 优先使用Props和Emits
  • 深层嵌套使用Provide/Inject
  • 复杂状态使用Pinia/Vuex
  • 避免过度使用Event Bus
最后更新:2026-03-26