父元素onclick在子元素点击后意外触发的JavaScript问题及优化方案
你遇到的这个问题核心是DOM事件冒泡机制在搞鬼。当你点击内部图片时,事件会从图片(子元素)开始向上冒泡到父元素main_el。
具体来说,当你在图片的点击回调里执行这些操作:
- 禁用图片的
onclick - 立刻给父元素
main_el绑定新的onclick
这时候,当前的点击事件还在冒泡流程中——事件已经触发了图片的回调,但还没走完向上冒泡到父元素的步骤。而你刚给父元素绑定的点击事件,会被这个正在冒泡的事件直接触发,这就是为什么父元素的onclick会意外执行。
你用setTimeout延迟绑定的方式能解决,本质是把父元素的事件绑定放到了当前事件循环之外,等冒泡流程完全结束后再绑定,自然就不会被触发了。
其实不用依赖延迟定时器,有几种更可靠的方式:
1. 阻止事件冒泡(最简单直接)
在图片的点击回调里,阻止事件继续向上冒泡到父元素。这样即使后续绑定父元素的事件,也不会被当前的点击事件触发:
function adjust_window_displayed(el, main_el){ var val = el.value; var img_el = document.getElementById("img_envelope"); if(val === "true"){ document.getElementById("display_mail_info").style.display = "block"; img_el.src = "https://co.uk/minimise.png"; img_el.style.cursor = "pointer"; main_el.style.cursor = "default"; main_el.onclick = function () { return false; }; el.value = "false"; img_el.onclick = function (e) { // 阻止事件冒泡 e.stopPropagation(); adjust_window_displayed(el, main_el); console.log("first"); }; console.log("first " + main_el.className); }else{ document.getElementById("display_mail_info").style.display = "none"; img_el.src = "https://dco.uk/envelope.png"; img_el.style.cursor = "default"; main_el.style.cursor = "pointer"; el.value = "true"; img_el.onclick = function (e) { // 阻止事件冒泡 e.stopPropagation(); return false; }; // 直接绑定,不用setTimeout main_el.onclick = function () { adjust_window_displayed(el, main_el); console.log("sec"); }; console.log("second " + main_el.className); } }
这里通过e.stopPropagation()阻止了点击事件从图片冒泡到父元素,所以在处理图片点击时,父元素不会收到这个事件,后续绑定父元素的事件也不会被触发。
2. 使用addEventListener/removeEventListener管理事件(更规范)
直接赋值onclick会覆盖之前的事件,而用事件监听API可以更灵活地添加/移除事件,同时结合命名函数,代码更清晰:
// 提前定义命名函数,方便添加/移除 function mainClickHandler() { adjust_window_displayed(el, main_el); console.log("sec"); } function imgClickHandler(e) { e.stopPropagation(); adjust_window_displayed(el, main_el); console.log("first"); } function adjust_window_displayed(el, main_el){ var val = el.value; var img_el = document.getElementById("img_envelope"); if(val === "true"){ document.getElementById("display_mail_info").style.display = "block"; img_el.src = "https://co.uk/minimise.png"; img_el.style.cursor = "pointer"; main_el.style.cursor = "default"; // 移除父元素的点击事件 main_el.removeEventListener('click', mainClickHandler); el.value = "false"; // 添加图片的点击事件 img_el.addEventListener('click', imgClickHandler); console.log("first " + main_el.className); }else{ document.getElementById("display_mail_info").style.display = "none"; img_el.src = "https://dco.uk/envelope.png"; img_el.style.cursor = "default"; main_el.style.cursor = "pointer"; el.value = "true"; // 移除图片的点击事件 img_el.removeEventListener('click', imgClickHandler); // 添加父元素的点击事件 main_el.addEventListener('click', mainClickHandler); console.log("second " + main_el.className); } }
这种方式避免了直接赋值onclick的覆盖问题,同时配合stopPropagation()阻止冒泡,逻辑更清晰,也不会有意外触发的问题。
3. 事件委托(更优雅,适合复杂场景)
如果页面上有多个类似的元素,或者元素会动态生成,可以用事件委托,把事件绑定到父元素或者更上层的元素,通过判断事件源来处理:
// 假设把事件委托到body或者更上层的容器 document.body.addEventListener('click', function(e) { var target = e.target; var main_el = document.getElementById('你的main_el的ID'); var el = document.getElementById('你的el的ID'); var img_el = document.getElementById("img_envelope"); if(target === main_el && el.value === "true"){ // 处理父元素点击放大的逻辑 document.getElementById("display_mail_info").style.display = "block"; img_el.src = "https://co.uk/minimise.png"; img_el.style.cursor = "pointer"; main_el.style.cursor = "default"; el.value = "false"; console.log("first " + main_el.className); }else if(target === img_el && el.value === "false"){ // 处理图片点击缩小的逻辑 e.stopPropagation(); document.getElementById("display_mail_info").style.display = "none"; img_el.src = "https://dco.uk/envelope.png"; img_el.style.cursor = "default"; main_el.style.cursor = "pointer"; el.value = "true"; console.log("second " + main_el.className); } });
事件委托的好处是不用频繁绑定/解绑事件,只需要一个监听就能处理所有相关的点击,也从根源上避免了事件冒泡带来的意外触发问题。
内容的提问来源于stack exchange,提问作者user9697902

