为何指定仅在特定视口宽度运行的hover函数缩放时异常?
问题:视口缩放后hover函数仍异常触发
我希望仅在视口宽度大于1400px时运行hover函数,但页面在小于1400px宽度加载时正常,从大于1400px缩放到小于该值时却异常。为何hoverMenuFn在窗口缩放时仍会运行?
现有代码
HTML
<div class="header"> <div class="menu"> <div class="menu-list-item menu-item-has-children">Item</div> <div class="menu-list-item menu-item-has-children">Item</div> <div class="menu-list-item menu-item-has-children">Item</div> <div class="menu-list-item menu-item-has-children">Item</div> </div> </div>
CSS
.menu-list-item { width: 100px; height: 40px; border: 3px solid pink; margin-bottom: 40px; } .open { background: lightpink; }
JavaScript
const body = document.querySelector("body"); const headerNav = document.querySelector(".header"); const menu = document.querySelector(".menu"); const menuListItems = document.querySelectorAll(".menu-list-item"); const menuListItemsWithChildren = document.querySelectorAll( ".menu-list-item.menu-item-has-children" ); const breakpoint = 1400; // Hover let hoverMenuFn = () => { let width = window.innerWidth; if (width <= breakpoint) { return false; } else { menuListItemsWithChildren.forEach((menuItem) => { menuItem.addEventListener("mouseenter", (event) => { console.log("mouse enter desktop"); menuItem.classList.add("open"); menuItem.setAttribute("aria-expanded", "true"); // Close other dropdowns const otherMenuItems = document.querySelectorAll(".open"); otherMenuItems.forEach((otherMenuItem) => { if (otherMenuItem !== event.target) { otherMenuItem.classList.remove("open"); } }); }); menuItem.addEventListener("mouseleave", (event) => { console.log("mouse leave"); menuItem.classList.remove("open"); menuItem.setAttribute("aria-expanded", "false"); }); }); } }; hoverMenuFn(); // Menu item click let clickFunction = () => { document .querySelectorAll(".menu-item-has-children .menu-link-level-1") .forEach((menuItem) => { menuItem.addEventListener("click", (event) => { console.log("click menu"); const parent = menuItem.parentNode; const isOpen = parent.classList.contains("open"); if (!isOpen) { parent.setAttribute("aria-expanded", "true"); parent.classList.add("open"); } else { parent.setAttribute("aria-expanded", "false"); parent.classList.remove("open"); } event.preventDefault(); // Close other dropdowns const otherMenuItems = document.querySelectorAll( ".open .menu-link-level-1" ); otherMenuItems.forEach((otherMenuItem) => { if (otherMenuItem !== event.target) { otherMenuItem.parentNode.classList.remove("open"); } }); }); }); }; // Mobile Navigation let mobileNavFn = () => { clickFunction(); let width = window.innerWidth; console.log(width); if (width <= breakpoint) { console.log("resized to mob"); headerNav.classList.add("mobile"); body.classList.add("scrolled"); if (!menu.classList.contains("menu--opened")) { menuListItems.forEach((menuListItem) => { menuListItem.classList.add("hide"); menuListItem.classList.remove("show"); }); } } else if (width >= breakpoint) { console.log("resized to desktop"); headerNav.classList.remove("mobile"); body.classList.remove("scrolled"); menuListItems.forEach((menuListItem) => { menuListItem.classList.add("show"); menuListItem.classList.remove("hide"); }); } }; mobileNavFn(); // Add event listeners for window resize and scroll window.addEventListener("resize", () => { mobileNavFn(); hoverMenuFn(); });
问题原因
当窗口宽度大于1400px时,hoverMenuFn会给每个子菜单项重复绑定mouseenter和mouseleave事件——每次触发resize且宽度达标时,都会新增一轮事件监听。而当窗口缩放到小于1400px时,函数只是提前return,但已经绑定的事件并不会自动移除,所以鼠标hover时这些事件依然会触发,导致异常行为。
另外,clickFunction也存在同样的重复绑定问题,每次resize都会给点击事件叠加监听。
解决方案
方案1:移除旧事件再绑定(具名函数实现)
将事件处理函数抽离为具名函数,这样可以在重新绑定前移除旧的监听:
const body = document.querySelector("body"); const headerNav = document.querySelector(".header"); const menu = document.querySelector(".menu"); const menuListItems = document.querySelectorAll(".menu-list-item"); const menuListItemsWithChildren = document.querySelectorAll( ".menu-list-item.menu-item-has-children" ); const breakpoint = 1400; // 抽离hover事件的具名函数 function handleMouseEnter(event) { const menuItem = event.currentTarget; console.log("mouse enter desktop"); menuItem.classList.add("open"); menuItem.setAttribute("aria-expanded", "true"); // Close other dropdowns const otherMenuItems = document.querySelectorAll(".open"); otherMenuItems.forEach((otherMenuItem) => { if (otherMenuItem !== menuItem) { otherMenuItem.classList.remove("open"); } }); } function handleMouseLeave(event) { const menuItem = event.currentTarget; console.log("mouse leave"); menuItem.classList.remove("open"); menuItem.setAttribute("aria-expanded", "false"); } // Hover let hoverMenuFn = () => { let width = window.innerWidth; // 先移除所有菜单项的hover事件监听 menuListItemsWithChildren.forEach((menuItem) => { menuItem.removeEventListener("mouseenter", handleMouseEnter); menuItem.removeEventListener("mouseleave", handleMouseLeave); }); if (width <= breakpoint) { return; } else { // 仅在宽度达标时重新绑定 menuListItemsWithChildren.forEach((menuItem) => { menuItem.addEventListener("mouseenter", handleMouseEnter); menuItem.addEventListener("mouseleave", handleMouseLeave); }); } }; hoverMenuFn(); // 同样处理点击事件的具名函数 function handleMenuItemClick(event) { console.log("click menu"); const parent = event.currentTarget.parentNode; const isOpen = parent.classList.contains("open"); if (!isOpen) { parent.setAttribute("aria-expanded", "true"); parent.classList.add("open"); } else { parent.setAttribute("aria-expanded", "false"); parent.classList.remove("open"); } event.preventDefault(); // Close other dropdowns const otherMenuItems = document.querySelectorAll( ".open .menu-link-level-1" ); otherMenuItems.forEach((otherMenuItem) => { if (otherMenuItem !== event.currentTarget) { otherMenuItem.parentNode.classList.remove("open"); } }); } // Menu item click let clickFunction = () => { // 先移除旧监听 document.querySelectorAll(".menu-item-has-children .menu-link-level-1") .forEach((menuItem) => { menuItem.removeEventListener("click", handleMenuItemClick); }); // 再绑定新监听 document.querySelectorAll(".menu-item-has-children .menu-link-level-1") .forEach((menuItem) => { menuItem.addEventListener("click", handleMenuItemClick); }); }; // Mobile Navigation let mobileNavFn = () => { clickFunction(); let width = window.innerWidth; console.log(width); if (width <= breakpoint) { console.log("resized to mob"); headerNav.classList.add("mobile"); body.classList.add("scrolled"); if (!menu.classList.contains("menu--opened")) { menuListItems.forEach((menuListItem) => { menuListItem.classList.add("hide"); menuListItem.classList.remove("show"); }); } } else if (width >= breakpoint) { console.log("resized to desktop"); headerNav.classList.remove("mobile"); body.classList.remove("scrolled"); menuListItems.forEach((menuListItem) => { menuListItem.classList.add("show"); menuListItem.classList.remove("hide"); }); } }; mobileNavFn(); // Add event listeners for window resize and scroll window.addEventListener("resize", () => { mobileNavFn(); hoverMenuFn(); });
方案2:使用事件委托(更高效)
把hover和点击事件都委托到父元素.menu上,这样无需重复绑定/移除事件,只需在事件触发时判断当前窗口宽度即可:
const body = document.querySelector("body"); const headerNav = document.querySelector(".header"); const menu = document.querySelector(".menu"); const menuListItems = document.querySelectorAll(".menu-list-item"); const breakpoint = 1400; // 事件委托处理hover menu.addEventListener("mouseenter", (event) => { const menuItem = event.target.closest(".menu-item-has-children"); if (!menuItem) return; // 仅在宽度大于断点时执行逻辑 if (window.innerWidth <= breakpoint) return; console.log("mouse enter desktop"); menuItem.classList.add("open"); menuItem.setAttribute("aria-expanded", "true"); // Close other dropdowns const otherMenuItems = document.querySelectorAll(".open"); otherMenuItems.forEach((otherMenuItem) => { if (otherMenuItem !== menuItem) { otherMenuItem.classList.remove("open"); } }); }); menu.addEventListener("mouseleave", (event) => { const menuItem = event.target.closest(".menu-item-has-children"); if (!menuItem) return; if (window.innerWidth <= breakpoint) return; console.log("mouse leave"); menuItem.classList.remove("open"); menuItem.setAttribute("aria-expanded", "false"); }); // 事件委托处理点击 menu.addEventListener("click", (event) => { const menuLink = event.target.closest(".menu-item-has-children .menu-link-level-1"); if (!menuLink) return; event.preventDefault(); console.log("click menu"); const parent = menuLink.parentNode; const isOpen = parent.classList.contains("open"); if (!isOpen) { parent.setAttribute("aria-expanded", "true"); parent.classList.add("open"); } else { parent.setAttribute("aria-expanded", "false"); parent.classList.remove("open"); } // Close other dropdowns const otherMenuItems = document.querySelectorAll( ".open .menu-link-level-1" ); otherMenuItems.forEach((otherMenuItem) => { if (otherMenuItem !== menuLink) { otherMenuItem.parentNode.classList.remove("open"); } }); }); // Mobile Navigation let mobileNavFn = () => { let width = window.innerWidth; console.log(width); if (width <= breakpoint) { console.log("resized to mob"); headerNav.classList.add("mobile"); body.classList.add("scrolled"); if (!menu.classList.contains("menu--opened")) { menuListItems.forEach((menuListItem) => { menuListItem.classList.add("hide"); menuListItem.classList.remove("show"); }); } } else if (width >= breakpoint) { console.log("resized to desktop"); headerNav.classList.remove("mobile"); body.classList.remove("scrolled"); menuListItems.forEach((menuListItem) => { menuListItem.classList.add("show"); menuListItem.classList.remove("hide"); }); } }; mobileNavFn(); // Add event listeners for window resize and scroll window.addEventListener("resize", mobileNavFn);
这种方式更简洁,也避免了重复绑定事件的问题,只需在事件触发时判断窗口宽度是否符合条件即可。
内容的提问来源于stack exchange,提问作者emilyyyyyy
相关产品推荐
相关产品推荐

