Vue3父子组件双向绑定和事件传递
1.为什么要写这个文章?
1.1 问题1:父子组件双向绑定
在工作中碰到了父子组件需要双向绑定的问题:父组件需要调用一个含有Modal
子组件,但是首次关闭子组件之后无法再次打开。
1.2 问题2:父子div事件传递
一个div里面有一个checkbox,需要实现点击div或者checkbox都可以改变checkbox的选择状态,但是点击checkbox的时候触发了两次改变事件的问题。
1.3 编写目的
本文记录了从遇到问题,思考问题,解决问题的过程,记录这个过程为后续学习借鉴和参考。
2. 情景复现-双向绑定
2.1父组件代码
<template>
<div class="bg size margin10">
<a-button type="primary" @click="openModal">打开</a-button>
<view-son :visible="visible"></view-son>
</div>
</template>
<script setup lang="ts">
import ViewSon from '@/views/test-page/view-son.vue';
import { ref } from 'vue';
const visible = ref<boolean>(true)
/**
* 打开弹窗
*/
const openModal = () => {
visible.value = true
console.log('点击了打开弹窗,visible的值为', visible.value);
}
</script>
<style scoped>
.bg {
background-color: white;
}
.size {
width: 100%;
height: calc(88vh);
}
.margin10 {
margin: 10px;
}
</style>
2.2子组件代码
<template>
<a-modal v-model:visible="sonVisible" @ok="handleOk" @cancel="handleCancel">
<div style="width: 200px; height: 200px;background-color: pink;">
i am a modal
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const props = defineProps({
visible: {
required: true,
type: Boolean
},
});
const sonVisible = ref<any>(props.visible);
const handleOk = () => {
sonVisible.value = false
}
const handleCancel = () => {
sonVisible.value = false
}
</script>
2.3页面效果
2.4 预期效果
此时我的父组件给子组件绑定了一个visible
的属性,我希望点击打开按钮之后就可以把弹窗打开,然后执行我需要的操作
2.5 实际效果
无论我点击确定还是取消之后,在父组件点击打开按钮的时候,子组件的弹窗始终无法再打开,并且查看控制台,父组件的visible
确实发生了改变
2.6 解决思路
2.6.1 梳理:visible
和v-model:visible
的区别
一开始觉得可能是因为写的 <view-son :visible="visible"></view-son>
中使用的是:visible="visible"
的原因,改为v-model:visible="visible"
,经过网上搜索,发现它们的区别如下:
:value
是属性绑定,用于将父组件的值传递给子组件,并且是只读的。这意味着当父组件的值改变时,子组件的值也会相应地改变,但是子组件无法直接修改这个值。v-model:value
是语法糖,它在内部相当于:value
和@input
的结合。它不仅可以将父组件的值传递给子组件,还可以在子组件中修改这个值并将修改后的值传递回父组件:value
用于单向数据流,将父组件的值传递给子组件;而v-model:value
用于双向数据绑定,既可以传递值给子组件,也可以接收子组件修改后的值。
改成双向绑定之后,再次尝试,发现实现效果还是没有发生改变,用vueDevtools查看,发现子组件的visible
已经是true
,但是绑定modal
的sonVisible
却还是false
2.6.2 新的发现
这个时候我发现了问题所在:props.visible
的值更新了,但是这个用ref
包装的sonVisible
却没有同步更新。既然发现问题了,那这就开始解决,不给我自动更新,那我就手动更新,用watch
来监听props.visible
的变化,如果props.visible
发生了改变,我就把这个值赋值给sonVisible
。
于是我在子组件里面加上了watch
如下
watch(() => props.visible, (newValue: boolean) => {
console.log('检测到props.visible发生改变,新值为', newValue);
sonVisible.value = newValue
})
再次尝试,结果发现果然没有这么顺利,点击打开按钮之后之后都没有触发到watch
2.6.3 深入思考
这个时候就很疑惑了,明明是v-model:visible="visible"
双向绑定的,但是更新之后都没有监测到父组件visible
的变化?
想着是既然是双向绑定,会不会是因为子组件关闭弹窗的时候没有通知父组件,导致组件之间的通信出现了某种问题?
2.6.4 解决问题
想到这里,我就又又又尝试了一下,把子组件原本只改变自己的值改为了顺带同步父组件的visible
子组件原代码
const handleOk = () => {
sonVisible.value = false
}
const handleCancel = () => {
sonVisible.value = false
}
子组件新代码(这里也可以采用
watch
来emit
)
const emit = defineEmits(['update:visible'])
const handleOk = () => {
sonVisible.value = false
emit('update:visible', sonVisible.value)
}
const handleCancel = () => {
sonVisible.value = false
emit('update:visible', sonVisible.value)
}
结果神奇的事情发生了,关闭父组件之后,居然能再次打开了,并且在子组件能watch
到父组件visible
的变化了
2.6.5 原理探索
带着满屏幕的疑惑,我打开了ChatGTP
,得到的答案是这样的:
想一想,好像也有点道理,具体Vue
内部是如何维护这些状态的无从得知,还需要进一步的探索
2.7 完整实现代码
2.7.1 父组件
<template>
<div class="bg size margin10">
<a-button type="primary" @click="openModal">打开</a-button>
<view-son v-model:visible="visible"></view-son>
</div>
</template>
<script setup lang="ts">
import ViewSon from '@/views/test-page/view-son.vue';
import { ref } from 'vue';
const visible = ref<boolean>(true)
/**
* 打开弹窗
*/
const openModal = () => {
visible.value = true
console.log('点击了打开弹窗,visible的值为', visible.value);
}
</script>
<style scoped>
.bg {
background-color: white;
}
.size {
width: 100%;
height: calc(88vh);
}
.margin10 {
margin: 10px;
}
</style>
2.7.2 子组件
<template>
<a-modal v-model:visible="sonVisible" @ok="handleOk" @cancel="handleCancel">
<div style="width: 200px; height: 200px;background-color: pink;">
i am a modal
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
const emit = defineEmits(['update:visible'])
const props = defineProps({
visible: {
required: true,
type: Boolean
},
});
const sonVisible = ref<any>(props.visible);
watch(() => props.visible, (newValue: boolean) => {
console.log('检测到props.visible发生改变,新值为', newValue);
sonVisible.value = newValue
})
const handleOk = () => {
sonVisible.value = false
emit('update:visible', sonVisible.value)
}
const handleCancel = () => {
sonVisible.value = false
emit('update:visible', sonVisible.value)
}
</script>
3.情景复现-事件传递
3.1 初始效果
div
里面包含了checkbox
<div style="width: 200px; height: 200px;background-color: pink;">
<a-checkbox v-model="check" @change="checkboxChange"/>
i am a modal
</div>
const checkboxChange = (value: any) => {
console.log('触发了checkbox的change事件');
}
这个时候在页面上自由点击checkbox
都是可以正常工作的
3.2 需求描述
这个时候我想实现一个功能,点击div
也可以快捷勾选checkbox
,心想这活我熟,加个@click
事件,收工
<div style="width: 200px; height: 200px;background-color: pink;" @click="clickDiv">
<a-checkbox v-model="check" @change="checkboxChange"/>
i am a modal
</div>
const clickDiv = () => {
check.value = !check.value
}
然后测试了一下发现,点击div
确实可以改变checkbox
的状态,到现在为止还是挺开心的,结果点checkbox
的时候坏事了,发现这东西点了之后没变化?!这时候发现问题好像有点严重了?
这个时候我给clickDiv
加了个console.log
看一下,发现点击checkbox
的时候div
的@click
事件和checkbox
的@change
事件居然同时触发了
const checkboxChange = (value: any) => {
console.log('触发了checkbox的change事件');
}
const clickDiv = () => {
console.log('触发了div的clickDiv事件');
check.value = !check.value
}
这时候我发现了,可能是div
包含了checkbox
,点击checkbox
的时候事件冒泡了,然后导致父级的div
的@click
事件也触发了
3.3 解决思路
经过我一波百度
找到了两种相对来说看起来比较可靠的方法
3.3.1 xxx.stop
比如要让@change
事件停止冒泡,可以采用@change.stop
这个时候我直接去checkbox
上加了个.stop
<a-checkbox v-model="check" @change.stop="checkboxChange" />
这个时候去页面上测试,发现不符合预期,不仅不触发checkbox
的@change
事件,还触发了div
的@click
事件,离大谱,可能是百度情报有误,这种方法行不通
3.3.2 event.stopPropagation()
可以先看一下这个链接,学习一下
stopPropagation()
经过上面第一种方法的折磨,我选择尝试第二种,调用事件的event.stopPropagation()
,
稍微改造一下代码:
<a-checkbox v-model="check" @change="checkboxChange" />
const checkboxChange = (value: any, event: Event) => {
console.log('触发了checkbox的change事件');
event.stopPropagation()
}
测试了一下,又测试了一下,最后再测试了一下,事不过三,我发现还是实现不了效果,又回到了最初的起点,checkbox
和div
的事件都同时触发了
3.3.3 思考问题
经过上面两次伟大的尝试,我发现还是没能实现效果,这个时候我打算看一下checkbox
和div
的event
,看看问题是不是在event
上面,又稍微改造了一下代码
<div style="width: 200px; height: 200px;background-color: pink;" @click="clickDiv">
<a-checkbox v-model="check" @change="checkboxChange" />
i am a modal
</div>
const checkboxChange = (value: any, event: Event) => {
console.log('触发了checkbox的change事件');
console.log('checkbox的event', event);
}
const clickDiv = (event: Event) => {
console.log('触发了div的clickDiv事件');
check.value = !check.value
console.log('div的event', event);
}
一开始怀疑是event上面没有stopPropagation()
导致调用失败,但是排查发现这个确实是存在的,所以就排除了这个猜想
再看它们两个event
的对比如下
div的event
checkbox的event
这个时候好像发现了一些不得了的东西,一个type是click
,一个type是stop
??!
会不会因为一开始给checkbox
设置的是@change.stop
,但是div上的事件是@click
,@change.stop
和@click
事件牛头不对马嘴,压根就没stop
住?
带着这个疑问,我又又又修改了一下代码,添加@click.stop
<div style="width: 200px; height: 200px;background-color: pink;" @click="clickDiv">
<a-checkbox v-model="check" @change="checkboxChange" @click.stop />
i am a modal
</div>
const checkboxChange = (value: any, event: Event) => {
console.log('触发了checkbox的change事件');
console.log('checkbox的event', event);
}
const clickDiv = (event: Event) => {
console.log('触发了div的clickDiv事件');
check.value = !check.value
console.log('div的event', event);
}
开始测试:
-
初始状态
-
点击页面
div
,只触发了div
的点击事件 -
点击
checkbox
,只触发了checkbox
的@change
事件
完工,实现效果
3.4 总结
.stop
确实是可以阻止事件冒泡的,但是只能阻止同类型的事件冒泡,所以在使用的时候需要注意到这一点,这也算是踩了个大坑
4. 总结
经过这一顿折腾,也算是加深了对父子组件数据双向绑定和stop
阻止事件冒泡的理解,但是也有一些遗留问题没有解决,需要进一步思考并完善:
- 父子组件双向绑定中,如果子组件没有
emit
导致的组件无法正常使用是否正如ChatGPT
所回答的一样,还是另有原因? .stop
和event.stopPropagation()
的区别以及为什么event.stopPropagation()
不生效的原因?event.stopPropagation()
的具体应用场景?
上述问题后续都需要解决
转载自:https://juejin.cn/post/7251518063274459173