一、子组件向父组件通信(子传父)
子组件通过触发自定义事件的方式向父组件传递数据,父组件通过监听该事件接收数据。
1.1 基本实现方式
子组件使用 emit 方法触发自定义事件,并传递数据;父组件在使用子组件时,通过 v-on(简写为 @)监听该事件。
子组件示例
<template>
<div>
<button @click="sendDataToParent">向父组件发送数据</button>
<input type="text" v-model="inputValue" @change="handleInputChange">
</div>
</template>
<script setup>
import { ref, defineEmits } from 'vue'
const inputValue = ref('')
// 定义可以触发的自定义事件,返回一个 emit 函数
const emit = defineEmits(['sendData', 'inputChange'])
// 点击按钮触发事件
const sendDataToParent = () => {
// 触发 sendData 事件,并传递数据(可以传递多个参数)
emit('sendData', '来自子组件的数据', 123)
}
// 输入框内容变化时触发事件
const handleInputChange = () => {
emit('inputChange', inputValue.value)
}
</script>
在 setup 语法中,需要通过 defineEmits 定义组件可以触发的事件,defineEmits 接收一个数组,数组元素为事件名称。返回的 emit 函数用于触发事件,第一个参数是事件名称,后续参数是要传递的数据。
父组件示例
<template>
<div>
<ChildComponent
@sendData="handleSendData"
@inputChange="handleInputChange"
/>
<p>从子组件接收的数据1:{{ childData1 }}</p>
<p>从子组件接收的数据2:{{ childData2 }}</p>
<p>输入框内容:{{ inputContent }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './components/ChildComponent.vue'
const childData1 = ref('')
const childData2 = ref('')
const inputContent = ref('')
// 处理 sendData 事件
const handleSendData = (data1, data2) => {
childData1.value = data1
childData2.value = data2
}
// 处理 inputChange 事件
const handleInputChange = (value) => {
inputContent.value = value
}
</script>
父组件通过 @事件名 监听子组件触发的事件,事件处理函数的参数对应子组件传递的数据。
1.2 事件验证
可以在 defineEmits 中对事件传递的数据进行验证,确保数据类型符合预期。
<script setup>
const emit = defineEmits({
// 验证事件传递的数据类型
sendNumber: (value) => {
if (typeof value === 'number') {
return true // 验证通过
} else {
console.warn('传递的数据必须是数字类型')
return false // 验证失败
}
}
})
const sendNumberData = () => {
emit('sendNumber', '这是一个字符串') // 会触发警告
emit('sendNumber', 456) // 验证通过
}
</script>
二、兄弟组件通信
兄弟组件之间没有直接的通信渠道,通常需要借助父组件作为中间媒介,或者使用全局事件总线、状态管理工具(如 Pinia)。这里介绍借助父组件的方式。
2.1 实现原理
- 组件 A(兄组件)通过子传父的方式将数据传递给父组件。
- 父组件接收数据后,通过父传子的方式将数据传递给组件 B(弟组件)。
父组件示例
<template>
<div>
<BrotherA @sendToParent="handleBrotherASend" />
<BrotherB :dataFromBrotherA="dataFromA" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import BrotherA from './components/BrotherA.vue'
import BrotherB from './components/BrotherB.vue'
const dataFromA = ref('')
// 接收 BrotherA 传递的数据
const handleBrotherASend = (data) => {
dataFromA.value = data
}
</script>
兄组件(BrotherA.vue)示例
<template>
<div>
<p>我是兄组件</p>
<button @click="sendToBrotherB">向弟组件发送数据</button>
</div>
</template>
<script setup>
import { defineEmits } from 'vue'
const emit = defineEmits(['sendToParent'])
const sendToBrotherB = () => {
emit('sendToParent', '来自兄组件的问候')
}
</script>
弟组件(BrotherB.vue)示例
<template>
<div>
<p>我是弟组件</p>
<p>从兄组件接收的数据:{{ dataFromBrotherA }}</p>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
const props = defineProps({
dataFromBrotherA: String
})
</script>
三、跨级组件通信
当组件层级较深时,使用 props 和 emit 进行通信会比较繁琐,此时可以使用 provide 和 inject 实现跨级通信。
3.1 基本使用
- 祖先组件使用 provide 提供数据或方法。
- 后代组件使用 inject 注入并使用这些数据或方法。
祖先组件示例
<template>
<div>
<p>祖先组件:{{ message }}</p>
<button @click="changeMessage">修改消息</button>
<ParentComponent />
</div>
</template>
<script setup>
import { ref, provide } from 'vue'
import ParentComponent from './components/ParentComponent.vue'
const message = ref('这是祖先组件提供的数据')
// 提供数据,第一个参数是注入的 key,第二个参数是提供的值
provide('messageKey', message)
// 提供方法
provide('changeMessageKey', () => {
message.value = '祖先组件数据已修改'
})
const changeMessage = () => {
message.value = '祖先组件主动修改数据'
}
</script>
父组件(中间层,不处理数据)示例
<template>
<div>
<p>父组件</p>
<ChildComponent />
</div>
</template>
<script setup>
import ChildComponent from './ChildComponent.vue'
</script>
后代组件示例
<template>
<div>
<p>后代组件</p>
<p>从祖先组件注入的数据:{{ injectedMessage }}</p>
<button @click="injectedChangeMessage">调用祖先组件的方法</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
// 注入数据,参数是祖先组件提供的 key
const injectedMessage = inject('messageKey')
// 注入方法
const injectedChangeMessage = inject('changeMessageKey')
</script>
provide 和 inject 可以跨越任意层级的组件,后代组件无论层级多深,都可以通过 inject 获取祖先组件提供的数据或方法。
3.2 处理默认值
当 inject 找不到对应的 key 时,可以设置默认值:
// 注入数据时设置默认值
const injectedData = inject('nonExistentKey', '默认值')
// 对于复杂类型的默认值,建议使用工厂函数,避免不必要的创建
const injectedObject = inject('objKey', () => ({ name: '默认名称' }))
四、不同通信方式的适用场景
通信方式 |
适用场景 |
优点 |
缺点 |
props / emit |
父传子、子传父(直接父子关系) |
简单直观,Vue 原生支持 |
层级较深时,需要逐层传递(props 透传问题) |
父组件中转 |
兄弟组件通信 |
实现简单,无需额外工具 |
组件关系复杂时,逻辑分散,维护困难 |
provide / inject |
跨级组件通信(层级较深) |
无需关心组件层级,直接传递 |
数据追踪困难,不适合频繁变化的数据 |
状态管理工具(如 Pinia) |
大型应用,多组件共享状态 |
集中管理状态,数据流向清晰 |
学习成本较高,小型应用可能过于复杂 |
全局事件总线 |
任意组件通信 |
灵活,可跨任意组件 |
事件命名容易冲突,缺乏类型检查,维护困难 |
在实际开发中,应根据组件关系和项目规模选择合适的通信方式。对于简单的父子组件通信,优先使用 props 和 emit;对于跨级通信,可考虑 provide 和 inject;对于大型应用的状态管理,建议使用 Pinia 等状态管理工具。
结语:
下一章我会讲解 Vue 3 中的状态管理工具 Pinia,包括它的基本概念、安装配置、核心用法以及在组件中的使用等内容,帮助宝子们更好地管理应用中的共享状态。如果你对前面的内容有疑问,或者想调整后续讲解的侧重点,请评论区留言。