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

相关推荐
m0_748230941 小时前
Redis 通用命令
前端·redis·bootstrap
YaHuiLiang1 小时前
一切的根本都是前端“娱乐圈化”
前端·javascript·代码规范
ObjectX前端实验室3 小时前
个人网站开发记录-引流公众号 & 谷歌分析 & 谷歌广告 & GTM
前端·程序员·开源
CL_IN3 小时前
企业数据集成:实现高效调拨出库自动化
java·前端·自动化
浪九天4 小时前
Vue 不同大版本与 Node.js 版本匹配的详细参数
前端·vue.js·node.js
qianmoQ4 小时前
第五章:工程化实践 - 第三节 - Tailwind CSS 大型项目最佳实践
前端·css
椰果uu5 小时前
前端八股万文总结——JS+ES6
前端·javascript·es6
微wx笑5 小时前
chrome扩展程序如何实现国际化
前端·chrome
~废弃回忆 �༄5 小时前
CSS中伪类选择器
前端·javascript·css·css中伪类选择器
CUIYD_19895 小时前
Chrome 浏览器(版本号49之后)‌解决跨域问题
前端·chrome