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时代的遗毒啊。拥抱数据驱动不够彻底,合该有此一劫

相关推荐
GISer_Jing13 分钟前
前端面试通关:Cesium+Three+React优化+TypeScript实战+ECharts性能方案
前端·react.js·面试
落霞的思绪1 小时前
CSS复习
前端·css
咖啡の猫3 小时前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲5 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5816 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路6 小时前
GeoTools 读取影像元数据
前端
ssshooter7 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry7 小时前
Jetpack Compose 中的状态
前端
dae bal8 小时前
关于RSA和AES加密
前端·vue.js
柳杉8 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化