一篇文章弄懂父子组件通信!
引言
在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