likes
comments
collection
share

Vue3父子组件双向绑定和事件传递

作者站长头像
站长
· 阅读数 37

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页面效果

Vue3父子组件双向绑定和事件传递

2.4 预期效果

此时我的父组件给子组件绑定了一个visible的属性,我希望点击打开按钮之后就可以把弹窗打开,然后执行我需要的操作

2.5 实际效果

无论我点击确定还是取消之后,在父组件点击打开按钮的时候,子组件的弹窗始终无法再打开,并且查看控制台,父组件的visible确实发生了改变

Vue3父子组件双向绑定和事件传递

2.6 解决思路

2.6.1 梳理:visiblev-model:visible的区别

一开始觉得可能是因为写的 <view-son :visible="visible"></view-son>中使用的是:visible="visible"的原因,改为v-model:visible="visible",经过网上搜索,发现它们的区别如下:

  1. :value是属性绑定,用于将父组件的值传递给子组件,并且是只读的。这意味着当父组件的值改变时,子组件的值也会相应地改变,但是子组件无法直接修改这个值。
  2. v-model:value是语法糖,它在内部相当于:value@input的结合。它不仅可以将父组件的值传递给子组件,还可以在子组件中修改这个值并将修改后的值传递回父组件
  3. :value用于单向数据流,将父组件的值传递给子组件;而v-model:value用于双向数据绑定,既可以传递值给子组件,也可以接收子组件修改后的值。

改成双向绑定之后,再次尝试,发现实现效果还是没有发生改变,用vueDevtools查看,发现子组件的visible已经是true,但是绑定modalsonVisible却还是false Vue3父子组件双向绑定和事件传递

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 Vue3父子组件双向绑定和事件传递

2.6.3 深入思考

这个时候就很疑惑了,明明是v-model:visible="visible"双向绑定的,但是更新之后都没有监测到父组件visible的变化? 想着是既然是双向绑定,会不会是因为子组件关闭弹窗的时候没有通知父组件,导致组件之间的通信出现了某种问题?

2.6.4 解决问题

想到这里,我就又又又尝试了一下,把子组件原本只改变自己的值改为了顺带同步父组件的visible

子组件原代码

const handleOk = () => {
  sonVisible.value = false
}

const handleCancel = () => {
  sonVisible.value = false
}

子组件新代码(这里也可以采用watchemit)

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的变化了

Vue3父子组件双向绑定和事件传递

2.6.5 原理探索

带着满屏幕的疑惑,我打开了ChatGTP,得到的答案是这样的: Vue3父子组件双向绑定和事件传递

想一想,好像也有点道理,具体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
}

Vue3父子组件双向绑定和事件传递

这时候我发现了,可能是div包含了checkbox,点击checkbox的时候事件冒泡了,然后导致父级的div@click事件也触发了

3.3 解决思路

经过我一波百度 Vue3父子组件双向绑定和事件传递 找到了两种相对来说看起来比较可靠的方法

3.3.1 xxx.stop

比如要让@change事件停止冒泡,可以采用@change.stop 这个时候我直接去checkbox上加了个.stop

<a-checkbox v-model="check" @change.stop="checkboxChange" />

这个时候去页面上测试,发现不符合预期,不仅不触发checkbox@change事件,还触发了div@click事件,离大谱,可能是百度情报有误,这种方法行不通 Vue3父子组件双向绑定和事件传递

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()
}

测试了一下,又测试了一下,最后再测试了一下,事不过三,我发现还是实现不了效果,又回到了最初的起点,checkboxdiv的事件都同时触发了 Vue3父子组件双向绑定和事件传递

3.3.3 思考问题

经过上面两次伟大的尝试,我发现还是没能实现效果,这个时候我打算看一下checkboxdivevent,看看问题是不是在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()导致调用失败,但是排查发现这个确实是存在的,所以就排除了这个猜想 Vue3父子组件双向绑定和事件传递

再看它们两个event的对比如下 div的event Vue3父子组件双向绑定和事件传递

checkbox的event Vue3父子组件双向绑定和事件传递

这个时候好像发现了一些不得了的东西,一个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);
}

开始测试:

  • 初始状态 Vue3父子组件双向绑定和事件传递

  • 点击页面div,只触发了div的点击事件 Vue3父子组件双向绑定和事件传递

  • 点击checkbox,只触发了checkbox@change事件 Vue3父子组件双向绑定和事件传递

完工,实现效果

3.4 总结

.stop确实是可以阻止事件冒泡的,但是只能阻止同类型的事件冒泡,所以在使用的时候需要注意到这一点,这也算是踩了个大坑

4. 总结

经过这一顿折腾,也算是加深了对父子组件数据双向绑定和stop阻止事件冒泡的理解,但是也有一些遗留问题没有解决,需要进一步思考并完善:

  • 父子组件双向绑定中,如果子组件没有emit导致的组件无法正常使用是否正如ChatGPT所回答的一样,还是另有原因?
  • .stopevent.stopPropagation()的区别以及为什么event.stopPropagation()不生效的原因?
  • event.stopPropagation()的具体应用场景?

上述问题后续都需要解决

转载自:https://juejin.cn/post/7251518063274459173
评论
请登录