一篇文章弄懂父子组件通信!
引言
在Vue中,父子组件的通信是组件间交互的基础。而父子组件间的通信又有好几种情况。本文将通过几个示例详细讲解如何在父子组件之间进行通信。
父子通信
父组件传值,子组件接收
在 Vue 中,父组件可以通过属性(props)向子组件传递数据,子组件使用 defineProps 接收这些数据。示例如下。
parent.vue
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
<Child :list="list" />
</template>
<script setup>
import { ref } from 'vue';
import Child from './child.vue'
const newMsg = ref('')
const list = ref(['html', 'css'])
const add = () => {
list.value.push(newMsg.value)
}
</script>
<style lang="css" scoped>
</style>
child.vue
<template>
<div class="body">
<ul>
<li v-for="item in props.list">{{item}}</li>
</ul>
</div>
</template>
<script setup>
const props = defineProps({
list:{
type:Array,
default: () => []
}
})
</script>
<style lang="scss" scoped>
</style>

我们可以看到当在输入框输入后,点击按钮触发事件,传给子组件的数组也随着改变了。
这是最常用的一种父子通信方式,本质就是父组件将每次修改后的数组传给子组件,子组件再渲染出来。其中父组件通过 v-model 绑定输入框的值 parentMessage,并将其传递给子组件的 message 属性。
子组件使用 defineProps 接收 message 并进行渲染。
父组件修改子组件属性
父组件通过修改子组件的 props 来触发子组件的更新。示例如下:
parent.vue
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
<Child :msg="val" />
</template>
<script setup>
import { ref } from 'vue';
import Child from './child.vue'
const newMsg = ref('')
const val = ref('')
const add = () => {
val.value = newMsg.value
}
</script>
<style lang="css" scoped>
</style>
child.vue
<template>
<div class="body">
<ul>
<li v-for="item in list">{{item}}</li>
</ul>
<!-- <p>{{ msg }}</p> -->
</div>
</template>
<script setup>
import {ref , computed, watch,onBeforeUpdate,onUpdated} from 'vue'
const list = ref(['html','css'])
const props = defineProps({
msg: {
type: String,
default: ''
}
})
// watch(
// () =>props.msg,
// (newVal,oldVal) => {
// list.value.push(newVal)
// }
// )
onBeforeUpdate(() => {
// console.log(props.msg); 在html用到的变量变更了就会执行
list.value.push(props.msg)
})
// onUpdated(() => {
// console.log(props.msg);
// list.value.push(props.msg)
// // 会一直执行,因为子组件中的list更改,onUpdated会一直执行
// })
</script>
<style lang="scss" scoped>
</style>
在这个示例中,我们在父组件的输入框绑定了 newMsg ,每当点击按钮触发了事件,就会同时去修改与子组件绑定的 msg 的值,当子组件的属性发生改变后触发响应,将修改后的值添加进要渲染的数组,达成效果。
而我们去实现发生改变有响应的事件,可以使用 watch去监听 或钩子函数。
-
watch是 Vue 的一个响应式系统,它用于监听数据的变化,并在变化时执行特定的操作。 这里我们是用watch正确追踪到props.msg的变化,再用回调函数,将新的props.msg值添加到list中,实现效果。 -
onBeforeUpdate是在组件即将被更新之前调用的钩子函数,onUpdated是在组件更新之后调用的钩子函数。onBeforeUpdate用于在组件即将更新时将props.msg的值推入list中。这是因为onBeforeUpdate会在任何响应式数据变化导致组件更新之前执行。而为什么不用
onUpdated是因为onUpdated钩子会在组件更新后触发,而在这个钩子中,我们又将props.msg推入list,这会导致组件再次更新,触发onUpdated钩子,再次将props.msg推入list,如此循环不止,陷入死循环。这便是用
onUpdated陷入死循环

如下便是使用 watch 或 onBeforeUpdate 实现的效果

provide 和 inject 跨层级传递
provide 和 inject 是 Vue 中用于跨层级组件传递数据的机制。但是只能允许祖先组件向后代组件传递数据,而且无需通过一层层的 props 传递。示例如下:
parent.vue
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
<Child />
</template>
<script setup>
import { ref, provide } from 'vue';
import Child from './child.vue';
const newMsg = ref('')
const list = ref(['html', 'css'])
const add = () => {
list.value.push(newMsg.value)
}
provide('list', list.value)
</script>
<style lang="css" scoped>
</style>
child.vue
<template>
<div class="body">
<ul>
<li v-for="item in list">{{item}}</li>
</ul>
</div>
</template>
<script setup>
import {inject} from 'vue';
const list = inject('list');
</script>
<style lang="scss" scoped>
</style>
在 Parent.vue 中,我们使用 provide 提供了 list 数组,这个 list 可以被所有后代组件注入和使用。
在 Child.vue 中,我们使用 inject 方法接收父组件 Parent 提供的 list ,并渲染。

子父通信
事件发布订阅机制
Vue中可以通过父子组件之间的事件订阅和发布,实现数据和事件的传递。示例如下:
parent.vue
<template>
<Child @addMsg="handle" />
<div class="body">
<ul>
<li v-for="item in list">{{item}}</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Child from './child.vue';
const newMsg = ref('')
const list = ref(['html', 'css'])
const handle = (e) => {
list.value.push(e)
}
</script>
<style lang="css" scoped>
</style>
child.vue
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</template>
<script setup>
import { ref,defineEmits } from 'vue';
const newMsg = ref('')
const emits = defineEmits(['addMsg']) // 定义事件
const add = () => {
emits('addMsg',newMsg.value) // 发布 ,顺序是先订阅后发布
}
</script>
<style lang="scss" scoped>
</style>
defineEmits 用于声明子组件可以触发的事件。
在这个示例中,本质上是子组件传值给父组件中的 list ,再在父组件中渲染出来。
-
子组件中
const emits = defineEmits(['addMsg'])定义了一个名为addMsg的事件。在add方法中,通过emits('addMsg', newMsg.value)触发addMsg事件,并传递newMsg.value作为参数。 当用户点击按钮时,add方法被调用,发布事件addMsg,并将输入框中的数据传递给父组件。 -
在父组件中,通过
@addMsg="handle"监听子组件触发的addMsg事件。 当子组件触发addMsg事件时,父组件的handle方法被调用。handle方法接收子组件传递的数据msg。 在handle方法中,将接收到的数据添加到list中再进行。
使用 v-model 绑定
这个相当于是上文中的事件订阅发布机制的一种优化。v-model 在 Vue 中通常用于双向绑定数据。在这个例子中,它将父组件的 list 数据与子组件的 list prop 绑定在一起,简化了数据的传递和更新逻辑。
parent.vue
<template>
<Child v-model:list="list" />
<div class="body">
<ul>
<li v-for="item in list">{{item}}</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Child from './child.vue'
const list = ref(['html', 'css'])
</script>
<style lang="css" scoped>
</style>
child.vue
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</template>
<script setup>
import { ref , defineProps,defineEmits} from 'vue'
const newMsg = ref('')
const emits = defineEmits(['update:list'])
const add = () => {
// props.list.push(newMsg.value)
// 不推荐
const arr = props.list
arr.push(newMsg.value)
emits('update:list',arr)
}
const props = defineProps({
list:[]
})
</script>
<style lang="scss" scoped>
</style>
在父组件中,使用 v-model:list="list" 将数据 list 绑定到子组件 Child 的 list prop
在子组件中,使用 defineEmits(['update:list']) 来定义组件可以触发的事件,并通过 emits('update:list', newMsg.value) 触发事件,并将 newMsg.value 作为参数传递给父组件,父组件的 list 会被更新,从而达成效果。
可以看到与事件订阅发布相比, parent.vue 简化了许多,方法都不需要了,只需要绑定就好了。
注意事项 不推荐直接修改
props的原因是Vue 强调单向数据流,即数据应该从父组件流向子组件。props是用于父子组件通信的一种机制,设计上是为了让父组件将数据传递给子组件,而子组件则负责接收这些数据并进行展示或处理。直接修改props违反了这种单向数据流的原则,因为它使得子组件可以修改由父组件传递来的数据,导致数据流动变得不明确和难以追踪。
父组件通过 ref 获取子组件实例
父组件可以通过 ref 获取子组件的实例,从而访问子组件暴露出来的数据和方法。此方式比较简单,直接在子组件中修改 list ,然后再暴露传递给父组件,父组件获取后再渲染。示例代码如下:
parent.vue
<template>
<Child ref="childRef" />
<div class="body">
<ul>
<li v-for="item in childRef?.list">{{item}}</li>
</ul>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
import Child from './child.vue'
import { ref } from 'vue'
const childRef = ref(null)
onMounted(() => {
console.log(childRef.value.list)
})
</script>
<style lang="css" scoped>
</style>
child.vue
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</template>
<script setup>
import { ref , defineExpose} from 'vue';
const newMsg = ref('')
const list = ref(['html', 'css'])
const add = () => {
list.value.push(newMsg.value)
}
defineExpose({ list })
// 子组件心甘情愿暴露出来list
</script>
<style lang="scss" scoped>
</style>
-
子组件使用
defineExpose暴露message数据。 -
父组件通过
ref获取子组件实例,并在showMessage方法中访问子组件的message数据
总结
通过以上示例,我们可以看到在 Vue 中父子组件通信的多种方式,每种方式都有特定的使用场景:
props和defineProps:用于父组件向子组件传递数据。- 修改子组件属性:父组件通过修改
props来触发子组件更新。 inject:用于祖先组件向后代组件传递数据。- 事件机制:子组件通过事件向父组件传递数据。
v-model和defineEmits:用于父组件与子组件之间的双向数据绑定。ref和defineExpose:用于父组件访问子组件的实例和数据。
理解和掌握这些通信方式,可以帮助我们在开发中更好地组织和管理 Vue 组件,如果觉得这篇文章对你有用,可以点个赞哦。
转载自:https://juejin.cn/post/7394722009430245426