Vue 3 组件通信,别只会用 Props 和 Emits 了,你想知道的都在这里
作为前端开发工程师,组件通信是Vue开发中绕不开的话题。虽然Props和Emits是最基础的通信方式,但在实际项目中,我们经常会遇到更复杂的场景。今天,我将带你全面了解Vue 3中14种组件通信方式,让你的开发技能更上一层楼!
📋 目录
- 父子组件通信(6种方式)
- 兄弟组件通信(3种方式)
- 跨层级通信(2种方式)
- 全局通信(2种方式)
- 特殊场景通信(3种方式)
- 完整对比总结表
一、父子组件通信(6种方式)
1️⃣ Props(父 → 子)
最基础的父传子方式,遵循单向数据流原则。
vue
<!-- 父组件 -->
<template>
<Child :msg="parentMsg" :count="count" />
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const parentMsg = ref('父组件数据')
const count = ref(10)
</script>
vue
<!-- 子组件 -->
<script setup>
const props = defineProps({
msg: String,
count: Number
})
</script>
适用场景:简单的父子数据传递
2️⃣ Emit(子 → 父)
子组件向父组件传递数据或触发事件。
vue
<!-- 子组件 -->
<template>
<button @click="handleClick">点击我</button>
</template>
<script setup>
const emit = defineEmits(['updateData', 'customEvent'])
const handleClick = () => {
emit('updateData', '子组件数据')
emit('customEvent', { id: 1, name: 'test' })
}
</script>
vue
<!-- 父组件 -->
<template>
<Child @update-data="handleUpdate" @custom-event="handleCustom" />
</template>
<script setup>
const handleUpdate = (data) => {
console.log('收到子组件数据:', data)
}
const handleCustom = (payload) => {
console.log('自定义事件:', payload)
}
</script>
适用场景:子组件需要通知父组件状态变化
3️⃣ v-model(双向绑定)
Vue 3中v-model的语法糖,实现父子组件数据双向绑定。
单值绑定(Vue 3.4+)
vue
<!-- 子组件 -->
<script setup>
const model = defineModel()
</script>
<template>
<input v-model="model" />
</template>
vue
<!-- 父组件 -->
<template>
<Child v-model="parentValue" />
</template>
<script setup>
import { ref } from 'vue'
const parentValue = ref('初始值')
</script>
多个v-model绑定
vue
<!-- 子组件 -->
<script setup>
const props = defineProps({
modelValue: String,
firstName: String,
lastName: String
})
const emit = defineEmits(['update:modelValue', 'update:firstName', 'update:lastName'])
</script>
vue
<!-- 父组件 -->
<template>
<Child
v-model="fullName"
v-model:first-name="firstName"
v-model:last-name="lastName"
/>
</template>
适用场景:表单组件、需要双向数据同步的场景
4️⃣ ref + defineExpose(父调用子组件方法)
父组件直接调用子组件的方法或访问子组件数据。
vue
<!-- 子组件 -->
<script setup>
import { ref } from 'vue'
const childData = ref('子组件数据')
const childMethod = () => {
console.log('子组件方法被调用')
}
// 必须暴露才能被父组件访问
defineExpose({
childData,
childMethod
})
</script>
vue
<!-- 父组件 -->
<template>
<Child ref="childRef" />
<button @click="callChildMethod">调用子组件方法</button>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
const callChildMethod = () => {
childRef.value.childMethod()
console.log('子组件数据:', childRef.value.childData)
}
onMounted(() => {
// 组件挂载后即可访问
console.log('子组件实例:', childRef.value)
})
</script>
适用场景:父组件需要直接操作子组件内部方法或数据
5️⃣ $attrs(属性透传)
透传父组件传递的、但子组件未声明的属性到更深层组件。
vue
<!-- 父组件 -->
<template>
<Child
class="custom-class"
style="color: red"
data-id="123"
@click="handleClick"
/>
</template>
vue
<!-- 子组件 -->
<template>
<!-- 透传所有未声明的属性到内部元素 -->
<div>
<input v-bind="$attrs" />
</div>
</template>
<script setup>
// 不需要声明props,$attrs会自动包含所有未声明的属性
</script>
适用场景:
- 封装第三方组件库
- 高阶组件开发
- 多层组件嵌套中的属性传递
6️⃣ 作用域插槽(子传父数据)
子组件向父组件传递数据,由父组件决定如何渲染。
vue
<!-- 子组件 -->
<template>
<div>
<slot :user="user" :logout="logout"></slot>
</div>
</template>
<script setup>
import { ref } from 'vue'
const user = ref({ name: '张三', age: 25 })
const logout = () => {
console.log('退出登录')
}
</script>
vue
<!-- 父组件 -->
<template>
<Child v-slot="{ user, logout }">
<div>
<p>用户名: {{ user.name }}</p>
<p>年龄: {{ user.age }}</p>
<button @click="logout">退出</button>
</div>
</Child>
<!-- 简写语法 -->
<Child #default="{ user }">
<p>{{ user.name }}</p>
</Child>
</template>
适用场景:
- 列表组件(如Table、List)
- 需要父组件自定义渲染逻辑的场景
二、兄弟组件通信(3种方式)
7️⃣ 共同父组件中转
通过父组件作为桥梁,实现兄弟组件通信。
vue
<!-- 父组件 -->
<template>
<BrotherA @data-change="handleDataChange" />
<BrotherB :sharedData="sharedData" />
</template>
<script setup>
import { ref } from 'vue'
import BrotherA from './BrotherA.vue'
import BrotherB from './BrotherB.vue'
const sharedData = ref('')
const handleDataChange = (data) => {
sharedData.value = data
}
</script>
适用场景:兄弟组件数量较少,逻辑简单
8️⃣ mitt(事件总线)
轻量级的事件发布/订阅库,实现任意组件通信。
安装和配置
bash
npm install mitt
javascript
// eventBus.js
import mitt from 'mitt'
export const emitter = mitt()
使用示例
vue
<!-- 组件A(发送方) -->
<script setup>
import { emitter } from '@/utils/eventBus'
const sendData = () => {
emitter.emit('data-update', { id: 1, value: '新数据' })
}
</script>
vue
<!-- 组件B(接收方) -->
<script setup>
import { emitter } from '@/utils/eventBus'
import { onUnmounted } from 'vue'
const handleDataUpdate = (data) => {
console.log('收到数据:', data)
}
// 监听事件
emitter.on('data-update', handleDataUpdate)
// 组件卸载时移除监听,避免内存泄漏
onUnmounted(() => {
emitter.off('data-update', handleDataUpdate)
})
</script>
适用场景:
- 跨层级组件通信
- 不想引入完整状态管理的轻量级场景
- 临时性通信需求
9️⃣ Pinia(状态管理)
Vue官方推荐的状态管理库,替代Vuex。
安装和配置
bash
npm install pinia
javascript
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
创建Store
javascript
// stores/userStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// state
const userInfo = ref({ name: '', age: 0 })
const token = ref('')
// getters
const isLogin = computed(() => !!token.value)
// actions
const setUserInfo = (data) => {
userInfo.value = data
}
const login = async (username, password) => {
// 模拟登录
token.value = 'xxx-token'
userInfo.value = { name: username, age: 25 }
}
const logout = () => {
token.value = ''
userInfo.value = { name: '', age: 0 }
}
return {
userInfo,
token,
isLogin,
setUserInfo,
login,
logout
}
})
组件中使用
vue
<script setup>
import { useUserStore } from '@/stores/userStore'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
// 解构保持响应式
const { userInfo, isLogin } = storeToRefs(userStore)
// 调用action
const handleLogin = () => {
userStore.login('zhangsan', '123456')
}
const handleLogout = () => {
userStore.logout()
}
</script>
适用场景:
- 全局状态共享(用户信息、购物车等)
- 复杂业务逻辑的状态管理
- 需要持久化或服务端同步的场景
三、跨层级通信(2种方式)
🔟 provide / inject(依赖注入)
祖先组件向任意后代组件注入数据,无需逐层传递。
vue
<!-- 祖先组件(App.vue) -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
const userInfo = ref({ name: '张三', role: 'admin' })
// 提供数据
provide('themeConfig', theme)
provide('userInfo', userInfo)
// 提供方法
provide('updateTheme', (newTheme) => {
theme.value = newTheme
})
</script>
vue
<!-- 中间层组件(不需要参与传递) -->
<template>
<div>
<GrandChild />
</div>
</template>
vue
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
// 注入数据
const theme = inject('themeConfig')
const userInfo = inject('userInfo')
// 注入方法
const updateTheme = inject('updateTheme')
const changeTheme = () => {
updateTheme('light')
}
</script>
适用场景:
- 主题配置、语言设置等全局配置
- 深层嵌套组件的数据共享
- 避免props drilling(属性逐层传递)
1️⃣1️⃣ $root(访问根实例)
通过根实例进行全局通信(不推荐,仅作了解)。
javascript
// 在任何组件中
const app = getCurrentInstance().appContext.app
app.config.globalProperties.$globalData = '全局数据'
注意:Vue 3中不推荐使用此方式,建议使用provide/inject或Pinia。
四、全局通信(2种方式)
1️⃣2️⃣ localStorage / sessionStorage
利用浏览器本地存储实现组件间通信。
javascript
// 存储数据
localStorage.setItem('userData', JSON.stringify({ name: '张三' }))
// 读取数据
const userData = JSON.parse(localStorage.getItem('userData'))
// 监听storage变化
window.addEventListener('storage', (e) => {
if (e.key === 'userData') {
console.log('数据变化:', e.newValue)
}
})
适用场景:
- 需要持久化的数据
- 跨页面/标签页通信
- 离线数据存储
1️⃣3️⃣ app.config.globalProperties
在应用级别添加全局属性。
javascript
// main.js
const app = createApp(App)
app.config.globalProperties.$api = {
getUser: () => { /* ... */ },
saveData: () => { /* ... */ }
}
app.mount('#app')
vue
<!-- 在任何组件中 -->
<script setup>
import { getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
proxy.$api.getUser()
</script>
适用场景:
- 全局工具函数
- 第三方库实例(如axios)
- 全局配置
五、特殊场景通信(3种方式)
1️⃣4️⃣ Teleport(传送门)
将组件渲染到DOM树的其他位置。
vue
<template>
<div>
<h1>主内容</h1>
<!-- 将模态框渲染到body下 -->
<Teleport to="body">
<div class="modal">
<p>这是一个模态框</p>
</div>
</Teleport>
</div>
</template>
适用场景:
- 模态框、弹窗
- 需要脱离父组件样式影响的组件
- 固定定位组件
1️⃣5️⃣ 动态组件通信
使用<component :is="">动态切换组件时的通信。
vue
<template>
<component
:is="currentComponent"
:data="sharedData"
@update="handleUpdate"
/>
</template>
<script setup>
import { ref, computed } from 'vue'
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
const currentComponent = ref('ComponentA')
const sharedData = ref({})
const components = {
ComponentA,
ComponentB
}
const handleUpdate = (data) => {
sharedData.value = data
}
</script>
适用场景:
- 选项卡切换
- 步骤向导
- 动态表单
1️⃣6️⃣ 自定义事件修饰符
为自定义组件添加v-model修饰符支持。
vue
<!-- 子组件 -->
<script setup>
const props = defineProps({
modelValue: String
})
const emit = defineEmits(['update:modelValue'])
const handleInput = (e) => {
let value = e.target.value
// 处理修饰符
if (props.modelModifiers?.trim) {
value = value.trim()
}
if (props.modelModifiers?.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
<template>
<input :value="modelValue" @input="handleInput" />
</template>
vue
<!-- 父组件 -->
<template>
<!-- 支持修饰符 -->
<CustomInput v-model.trim.capitalize="value" />
</template>
📊 完整对比总结表
| 通信方式 | 适用场景 | 优点 | 缺点 | 推荐指数 |
|---|---|---|---|---|
| Props | 父→子 | 简单、官方推荐、类型安全 | 只能父子使用 | ⭐⭐⭐⭐⭐ |
| Emit | 子→父 | 简单、符合单向数据流 | 只能父子使用 | ⭐⭐⭐⭐⭐ |
| v-model | 双向绑定 | 语法简洁、语义清晰 | 需要配合props/emits | ⭐⭐⭐⭐ |
| ref+expose | 父调子方法 | 直观、灵活 | 只能父对子 | ⭐⭐⭐⭐ |
| $attrs | 属性透传 | 避免中间层冗余代码 | 可读性稍差 | ⭐⭐⭐ |
| 作用域插槽 | 子传父数据 | 灵活、父组件控制渲染 | 语法稍复杂 | ⭐⭐⭐⭐ |
| mitt | 任意组件 | 轻量、解耦 | 需手动管理订阅 | ⭐⭐⭐⭐ |
| Pinia | 全局状态 | 集中管理、类型安全 | 学习成本 | ⭐⭐⭐⭐⭐ |
| provide/inject | 跨层级 | 避免props drilling | 松散耦合 | ⭐⭐⭐⭐ |
| localStorage | 持久化 | 跨页面、持久化 | 同步问题 | ⭐⭐⭐ |
| Teleport | DOM位置 | 灵活渲染位置 | 特定场景 | ⭐⭐⭐ |
💡 实战建议
选择通信方式的原则
- 优先使用Props/Emit:简单场景首选,符合Vue设计哲学
- 避免过度使用全局状态:不是所有数据都需要放入Pinia
- 考虑组件复用性:选择通用性更强的通信方式
- 关注性能:避免不必要的响应式数据传递
常见误区
❌ 滥用provide/inject:不要用它替代props,它更适合配置类数据
❌ 过度依赖事件总线:mitt适合轻量级场景,复杂业务用Pinia
❌ 忘记清理事件监听:使用mitt时记得在onUnmounted中移除监听
❌ 暴露过多子组件内部:defineExpose只暴露必要的API
🎯 总结
Vue 3提供了丰富的组件通信方式,每种方式都有其适用场景:
- 简单父子通信:Props + Emit
- 双向绑定:v-model
- 父调子方法:ref + defineExpose
- 跨层级通信:provide/inject
- 全局状态:Pinia
- 轻量级任意通信:mitt
- 灵活渲染:作用域插槽
掌握这些通信方式,能让你在面对不同业务场景时游刃有余。记住,没有最好的方式,只有最合适的方式!
互动话题:你在项目中遇到过哪些复杂的组件通信场景?是如何解决的?欢迎在评论区分享你的经验!🚀
本文基于Vue 3.4+版本编写,代码示例均经过实际测试。如有疑问,欢迎交流讨论!