You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何指定仅在特定视口宽度运行的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 13:37:02