checkbox选中状态失效之谜

大家好,我是老纪。

前两天遇到一个小Bug,感觉有点儿意思,简单记录下。

这是一个原生的checkbox,功能就是勾选或取消:

代码很简单,左边是原生的input框,右侧是一个label标签:

vue 复制代码
<div class="uno-bg-color mt-1" v-show="!isFold">
    <div v-for="(item, index) in menuItems" :key="item.name" class="flex flex-col flex-items-start">
        <span class="text-sm color-white ml-2" :class="index === 0 ? 'mt-3' : ''"> {{ item.name }}</span>
        <ul class="list-none ml--5">
            <li v-for="name in item.children" :key="name" class="text-align-left text-sm text-white mb-2">
                <input class="mr-2" type="checkbox" :id="name" :name="name" :value="name"
                    :checked="isSelected(name)" @click="change" />
                <label :for="name" class="cursor-pointer">{{ name }}</label>
            </li>
        </ul>
    </div>
</div>

谁成想与其他组同事的代码集成(动态加载他们的代码)后,点了死活没有反应了。

这时有两种可能,一种是 JS 层面,点击事件被拦截了;另一种是 CSS,原生的勾选样式被覆盖了。

从道理上讲,JS 的概率更大些(因为后者我也不知道怎么做)。

经过断点追踪,发现我的click居然没有触发!

在浏览器里找到一个input元素,在右侧Event Listeners里查看绑定的事件:

click事件的第二个,是我们的代码,而第一个则是集成的代码。展开看下是某个组件的处理:

也就是说,我的点击被这个全局事件坑了:

ts 复制代码
 document.addEventListener("click", (e) => {
    e.preventDefault();
    // xxx
 }, false);

浏览器控制台界面上有个Remove的按钮,点击后就移除了这个事件,我的功能就正常了。

于是给写这段代码的同事说,『不要这么做啊,能不能把它去掉』?正好这个业务在与我们集成时并不需要,同事很爽快地去掉了。

问题解决!

但我回头一想,不对劲啊!

我们知道,正常的点击事件是冒泡的,我点击checkbox时,理论上应该先触发它的onclick事件,再往上层冒泡。那么为什么还会出现我的代码被拦截的情况呢?

事件委托

我起始的想法是:Vue应该与React一样,用事件委托(代理),在根元素上绑定事件,以优化性能;当我加载同事代码时,因为某种顺序问题,先执行了他的代码,导致我的代码被拦截了。

但再看这个截图,明显这个事件是绑定在具体元素(这里是input)上的啊,难道浏览器会出错?

所以,Vue3并没有使用事件委托进行事件合成。

我在知乎这个问题下看的答案,表示现代浏览器不需要再做这方面优化,不知道靠不靠谱:

言归正传。

我们在元素的事件处理里右键菜单:

选中显示函数定义的地方,会跳转到Vue的源码:

打断点追踪也确实只是普通元素的事件绑定。

坑在何处

既然Vue3并没有使用事件委托,那么为什么会出现这种冲突的情况呢?

我心中窃喜,又一个探(装)案(B)的机会来了!

我有种预感,真相可能并不复杂。在我的辛苦努力下,终于发现了华点。

大家仔细看这段代码:

vue 复制代码
<input class="mr-2" type="checkbox" :id="name" :name="name" :value="name"
                    :checked="isSelected(name)" @click="change" />
<label :for="name" class="cursor-pointer">{{ name }}</label>

我为每个input框设置了一个id,原因是为了点击label时也能起到选中checkbox一样的效果,for属性是关键。

为什么不将click事件放在父级div上呢?因为我在change事件中要获取input元素当前的选中状态:

ts 复制代码
function change(e: MouseEvent) {
    const target = e.target as HTMLInputElement;
    const value = target.value;
    const checked = target.checked;
    // do else
}

正是这样一个处理,引发了此次的问题。

为方便大家理解,我写个更简单的示例:

vue 复制代码
<script setup lang="ts">
  function handleClick() {
    console.log('--111----');
  }

</script>

<template>
  <div>
    <input type="checkbox" id="test" @click="handleClick" />
    <label for="test">点击我</label>
  </div>

</template>

再来一段全局事件注册:

ts 复制代码
document.addEventListener(
  "click",
  (e) => {
    console.log("----222---");
    e.preventDefault();
    // xxx
  },
  false
);

页面是这样的朴实无华:

注意点击左侧checkbox

这时打印结果符合预期------由于preventDefault的存在,checkbox的选中状态没有变化。

但当点击label时,坑爹的来了!

只触发了document的事件!连checkbox上的click事件都没有触发!

这也正是这个问题的狡猾之处,因为大部分情况我都是点击的label,而非checkbox本身!

如果仅仅是checkbox click执行后,样式没有变化,那么问题反倒简单了,阻止事件冒泡就可以了。

如果去除document事件中的e.preventDefault(),打印就有趣了:

顺序居然是:document-> input -> document

也就是说,labelcheckbox的绑定,会先触发全局点击事件,如果没有被阻止,会再触发绑定元素的事件,继而冒泡。而checkbox本身的点击则遵循正常的冒泡逻辑。

不吃一堑,难长一智啊!

优化方案

找到了罪魁祸首,下来的处理就非常简单了。

  1. 去除labelinput的绑定,老老实实往父级div上注册事件,至于checkbox的状态,完全可以在内存中记录。
  2. 事件停止冒泡,添加.stop修饰符即可。
vue 复制代码
<script setup lang="ts">
import { ref } from 'vue';

const checked = ref(false);

function handleClick() {
  checked.value = !checked.value;
  console.log('--111----');
}

</script>

<template>
  <div @click.stop="handleClick">
    <input type="checkbox" id="test" v-model="checked" />
    <label>点击我</label>
  </div>

</template>

这时再怎么点击,都不会触发document的事件了。

总结

本文讲述了我在开发过程中遇到的一个小Bug以及解决过程。

首先,我展示了一个使用原生checkboxlabel标签的简单Vue代码,并描述了在与同事代码集成后出现点击无响应的问题。经过分析,发现是由于全局事件监听器中的preventDefault导致的点击事件被拦截。虽然我让同事将全局拦截的代码去掉了解决了问题,但同时引发思考,正常来说不应该是先冒泡吗?为什么checkboxclick事件没有触发?

接下来,我们简单看了下Vue3React在事件处理上的不同,Vue3并没有使用事件委托,将事件绑定在根元素上。这可能与现代浏览器的优化有关,毕竟事件委托各有利弊。因为并非本文重点,笔者没有深究。

我通过简单示例和调试过程展示了问题根源:在document事件preventDefault的情况下,labelcheckbox的绑定,会先触发全局点击事件,如果没有被阻止,会再触发绑定元素的事件,继而冒泡。而checkbox本身的点击则遵循正常的冒泡逻辑。

最终的解决方案是将点击事件直接绑定在父级div上,并使用.stop修饰符停止事件冒泡,以确保点击div时不会触发全局事件。

事后想想,我的事件处理中,使用target.checked来获取状态,还是jQuery时代的遗毒啊。拥抱数据驱动不够彻底,合该有此一劫

相关推荐
傻虎贼头贼脑6 分钟前
day21JS-npm中的部分插件使用方法
前端·npm·node.js
low神17 分钟前
前端在网络安全攻击问题上能做什么?
前端·安全·web安全
qbbmnnnnnn1 小时前
【CSS Tricks】如何做一个粒子效果的logo
前端·css
唐家小妹1 小时前
【flex-grow】计算 flex弹性盒子的子元素的宽度大小
前端·javascript·css·html
涔溪1 小时前
uni-app环境搭建
前端·uni-app
安冬的码畜日常1 小时前
【CSS in Depth 2 精译_032】5.4 Grid 网格布局的显示网格与隐式网格(上)
前端·css·css3·html5·网格布局·grid布局·css网格布局
洛千陨1 小时前
element-plus弹窗内分页表格保留勾选项
前端·javascript·vue.js
小小19921 小时前
elementui 单元格添加样式的两种方法
前端·javascript·elementui
前端没钱2 小时前
若依Nodejs后台、实现90%以上接口,附体验地址、源码、拓展特色功能
前端·javascript·vue.js·node.js
爱喝水的小鼠2 小时前
AJAX(一)HTTP协议(请求响应报文),AJAX发送请求,请求问题处理
前端·http·ajax