我写了一个非常无语的bug

1,396 阅读4分钟

大家好,我是前端小张同学,最近忙于工作都没有时间更新文章了,今天跟大家分享一个我在工作写的bug吧,其实没有什么技术含量,但是是一个值得深思的问题。

1:需求描述

需求是,点击一个 盒子DOM,然后需要打开一个弹窗,并且弹窗 弹窗可以是任意的,但弹窗里有一个按钮,点击按钮可以去关闭这个弹层,大概的需求就是这样子。为了更好的展示,下方是示例

image.png

2:神奇的问题

1:点击确认 无法关闭弹窗?

2:点击确认后 关闭了,但第二次无法重新唤起。

3: 代码

这个项目使用vue3写的一个需求,做一个银行卡选择列表,是以 webView 的形式嵌入在App中,当然在这里的代码是demo示例。

index.vue

此文件 内容主要是放入了一个弹层组件以及,可以去点击 class 为 wapper dom 进行打开 actionSheet 弹层。

<template>
  <div class="wapper" @click="clickHandle">
    <div class="item">
      <actionSheet v-model="showModal" />
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import actionSheet from './components/actionSheet.vue';
const showModal = ref(false)
const clickHandle = ()=> {
	console.log('chufa1');
	showModal.value = true
}
watch(()=> showModal.value, (newVal)=> {
	console.log('最新值更新', newVal);
})

</script>

<style scoped lang="less">
.wapper{
  height: 100vh;
  .item{
    width: 100%;
    height: 80px;
    background-color: #ccc;
  }
}
</style>

到这里,你可以先简单知道它DOM树结构是什么,我们继续向下看。

actionSheet.vue

此文件 里面放了一个二次封装的弹层组件,其实到这里你可以看出来,真正的弹层组件应该是 drawer, 该组件在监听 index.vue v-mode 传入的 modelValue 当 modelValue 发生变化时,在组件内部维护一个状态给到 drawer 组件,当 vmOpen 为 true 时 则可以展示 弹层。

<template>
  <drawer
    v-model:visible="vmOpen"
    class="custom-class"
    btn-text="确认"
    @confrim="closeModal"
  >
    <p>Some contents...</p>
    <p>Some contents...</p>
  </drawer>
</template>
<script lang="ts" setup>
import { defineEmits, defineProps, ref, watch } from 'vue';
import drawer from './drawer.vue';
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
	modelValue: {
		type: Boolean,
    
	}
})
const vmOpen = ref<boolean>(false);

const closeModal = ()=> {
	vmOpen.value = false 
	emit('update:modelValue', false)
}
watch(()=> props.modelValue, (newVal)=> {
	vmOpen.value = newVal
})
</script>

darwer.vue

<template>
  <div v-show="visible" class="drawer-wapper">
    <div class="tanceng">
      <slot name="default" />
      <button v-if="btnText" class="close" @click="emit('confrim')">{{ btnText }}</button>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { defineEmits, defineProps, onMounted } from 'vue';
const emit = defineEmits(['confrim'])
const props = defineProps({
	visible: {
		type: Boolean,
		default: false
	},
	btnText: {
		type: String,
		default: ''
	}
})
onMounted(()=> {
	console.log('props', props.visible);
})
</script>

<style scoped lang="less">
.drawer-wapper{
  width: 100%;
  height: 100%;
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  background-color: rgb(0 0 0 / 30%);

  .tanceng{
    width: 100%;
    height: 200px;
    position: absolute;
    bottom: 0;
    animation: trans linear .3s ;
    background-color: #fff;

    .close{
      width: 100%;
    }
  }

  @keyframes trans {
    0%{
      transform: translateY(100%);
    }

    100%{
      transform: translateY(0%);
    }
  } 
}
</style>

ok,到这里 ,捋一下逻辑,大概是这个样子 。

image.png

看完上方逻辑图,你应该了解 index.vue 绑定了一个 showModal 状态 ,点击 dom节点更改 showModal 值为 true, 当 showModal 值发生变化时,actionSheet组件 监听变化,保存一个组件内部的状态,当组件内部状态vmOpen发生变化时 为true 时,弹层就展示了,当点击弹层组件里的确认发射 @confirm 事件去关闭弹层。这样整体下来思路没什么问题,完美形成了一个闭环,可奇怪的是,这个 弹层组件关闭不掉,请看下方。

好的,我的掘友们,到这里你可以综合上方代码去发现一下,为什么不生效,让时间停在这里,想一下哪里有问题。

scrollView.gif

解答

其实大家,如果没有想明白为什么,你可以 拿着我的代码去跑一个demo 你就明白了,其实最根本的原理就是 drawer 组件内部 发射事件之前 没有做 冒泡处理,但我们 actionSheet closeModal函数中 也没有进行点击事件的 阻止冒泡,stopProgation(),导致它又触发了 外层的 index.vue clickHandle 事件 所以无法关闭,虽然这个bug很普通,但涉及的知识点也算是还可以。

知识点

1:事件冒泡,事件捕获的过程

2:弹层组件的实现以及原理

3:v-model的原理及本质

4:vue3中 如何更新 v-mode的值

拓展

在上诉代码中,有没有发现我们 watch 监听值非常的麻烦,还需要组件内部在定义一个变量,再保存。

在这里就要跟大家讲解vueUse中 新的 Hooks了 叫 useVModdel

vueUse官方文档

它的作用是什么?

它可以帮助我们,通过赋值的形式进行触发 update:xxx 更新v-model的值。

Usage 官方示例

import { useVModel } from '@vueuse/core'

export default {
  setup(props, { emit }) {
    const data = useVModel(props, 'data', emit)

    console.log(data.value) // props.data
    data.value = 'foo' // emit('update:data', 'foo')
  },
}

我们在setup语法糖中的用法,可以这样

<template>
  <drawer
    v-model:visible="vmOpen"
    class="custom-class"
    btn-text="确认"
    @confrim="closeModal"
  >
    <p>Some contents...</p>
    <p>Some contents...</p>
  </drawer>
</template>
<script lang="ts" setup>
import { useVModel } from '@vueuse/core';
import { defineEmits, defineProps, watch } from 'vue';
import drawer from './drawer.vue';

const emit = defineEmits(['update:modelValue'])
const props = defineProps({
	modelValue: {
		type: Boolean,
    
	}
})
const vmOpen = useVModel(props, 'modelValue', emit)  // 将你的 props 和 emit 以及 需要转换的key 传入
// const vmOpen = ref<boolean>(false);

const closeModal = (e:any)=> {
	e.stopPropagation();
	vmOpen.value = false  // 同等与 emit('update:modelValue' , false)
	emit('update:modelValue', false)
}
watch(()=> props.modelValue, (newVal)=> {
	vmOpen.value = newVal
})
</script>

原理

useVModel 使用了 computed 进行中间代理,当你设置值的时候,他会自动帮你派发 emit 事件,对于开发是一个较好的选择。

image.png

本文到这里就结束啦,谢谢,我是前端小张同学