1. Простой случай: удаление обработчика напрямую
- Когда использовать:
Если обработчик — это заранее объявленная функция с фиксированной ссылкой, и вы можете её легко переиспользовать при удалении. - Как работает:
При добавлении слушателя черезaddEventListenerвы передаёте функцию. Чтобы удалить этот слушатель, нужно передать точно ту же функцию вremoveEventListener.
// Объявляем функцию-обработчик
function clickHandler(event) {
console.log("Элемент был кликнут");
}
// Добавляем обработчик
element.addEventListener("click", clickHandler);
// Позже удаляем обработчик
element.removeEventListener("click", clickHandler);
2. Случай с использованием Map: динамические обработчики для нескольких элементов
- Когда использовать:
Если вы добавляете обработчики для множества элементов, и для каждого элемента создаётся уникальная функция (например, внутри цикла).
При этом ссылка на функцию создаётся “на лету” (например, через анонимную функцию) и без хранения вы не сможете потом удалить слушатель, так как ссылку на неё потеряете. - Как работает:
Использование структурыMapпозволяет сохранить ассоциацию между элементом и его обработчиком. Это особенно полезно, когда:- Обработчики создаются динамически.
- Понадобится изменить или удалить слушатель при изменении условий (например, при изменении ширины экрана).
// Получаем коллекцию элементов
const elements = document.querySelectorAll(".item");
// Создаём Map для хранения обработчиков
const handlersMap = new Map();
// Добавляем обработчик для каждого элемента и сохраняем ссылку в Map
elements.forEach(item => {
const handler = () => {
console.log("Элемент был кликнут");
};
// Сохраняем ссылку на обработчик, связав её с элементом
handlersMap.set(item, handler);
item.addEventListener("click", handler);
});
// Позже удаляем обработчики, используя сохранённые ссылки
elements.forEach(item => {
const handler = handlersMap.get(item);
item.removeEventListener("click", handler);
});
Мой пример
1. Использование Map для хранения обработчиков подменю
- Почему Map:
При динамическом создании обработчиков для каждого элемента меню возникает проблема: если функция создаётся «на лету» (например, анонимная функция внутри цикла), её нельзя будет удалить черезremoveEventListener, так как ссылка на неё будет потеряна. Чтобы избежать этого, вы сохраняете созданные обработчики в структуреMap. Ключом является сам элемент меню, а значением – объект с тремя функциями:
const submenuHandlers = new Map();
Создание и сохранение обработчиков:
Внутри функции updateSubmenuEvents для каждого элемента меню проверяется, есть ли уже сохранённый набор обработчиков. Если нет, создаются:
if (!submenuHandlers.has($menuItem)) {
const openHandler = () => handleSubmenuOpen($menuItem);
const closeHandler = () => handleSubmenuClose($menuItem);
const clickHandler = (e) => {
const $link = $menuItem.querySelector('a');
const $subMenu = $menuItem.querySelector('.sub-menu');
if (!$link || !$subMenu) return;
if (!$subMenu.classList.contains('open')) {
e.preventDefault();
$subMenu.classList.add('open');
}
// иначе — переход разрешён
};
submenuHandlers.set($menuItem, {
openHandler,
closeHandler,
clickHandler,
});
}
Таким образом, для каждого элемента меню создаются и сохраняются ссылки на функции-обработчики.
2. Функция updateSubmenuEvents: удаление и повторное назначение обработчиков
- Удаление обработчиков:
Перед повторным назначением обработчиков для каждого элемента меню выполняется удаление уже привязанных обработчиков:
$menuItem.removeEventListener('mouseover', openHandler);
$menuItem.removeEventListener('mouseout', closeHandler);
$menuItem.removeEventListener('click', clickHandler);
Это гарантирует, что если ранее слушатели уже были добавлены, они не будут оставаться “подвешенными” и не вызовут нежелательных эффектов (например, дублирование вызовов).
Назначение новых обработчиков в зависимости от ширины экрана:
После удаления обработчиков, на основании текущей ширины экрана, добавляются нужные события:
- Для экранов шире 1023.5px:
Привязываются обработчикиmouseoverиmouseoutдля открытия и закрытия подменю. - Для узких экранов:
Привязывается обработчикclick, который предотвращает переход по ссылке и открывает подменю, если оно ещё не открыто.
if (screenWidth > 1023.5) {
$menuItem.addEventListener('mouseover', openHandler);
$menuItem.addEventListener('mouseout', closeHandler);
} else {
$menuItem.addEventListener('click', clickHandler);
}
Весь код:
const header = () => {
const SELECTORS = {
header: '.js-header',
menuTrigger: '.js-header-menu-trigger',
headerNav: '.js-header-nav',
headerNavItems: '.js-header-nav .header-menu > .menu-item'
};
const CLASSNAMES = {
bodyOpenMenuState: 'body--open_menu_state',
headerScrollState: 'header--scroll_state',
headerNavState: 'header__nav--active'
};
const $body = document.body;
const $header = document.querySelector(SELECTORS.header);
const $menuTriggers = document.querySelectorAll(SELECTORS.menuTrigger);
const $headerNav = document.querySelector(SELECTORS.headerNav);
const $headerNavItems = document.querySelectorAll(SELECTORS.headerNavItems);
let isMenuOpen = false;
if (!$header) return;
const handleSubmenuOpen = ($menuItem) => {
const $subMenu = $menuItem.querySelector('.sub-menu');
if ($subMenu) {
$subMenu.classList.add('open');
}
};
const handleSubmenuClose = ($menuItem) => {
const $subMenu = $menuItem.querySelector('.sub-menu');
if ($subMenu) {
$subMenu.classList.remove('open');
}
};
const headerScroll = () => {
const scrollY = window.scrollY;
if (scrollY > 10) {
$header.classList.add(CLASSNAMES.headerScrollState);
} else {
$header.classList.remove(CLASSNAMES.headerScrollState);
}
};
const handleTriggerClick = () => {
$body.classList.toggle(CLASSNAMES.bodyOpenMenuState);
$headerNav?.classList.toggle(CLASSNAMES.headerNavState);
isMenuOpen = !isMenuOpen;
};
// храним ссылки на обработчики, чтобы их можно было удалять
const submenuHandlers = new Map();
const updateSubmenuEvents = () => {
const screenWidth = window.innerWidth;
$headerNavItems.forEach(($menuItem) => {
if (!submenuHandlers.has($menuItem)) {
const openHandler = () => handleSubmenuOpen($menuItem);
const closeHandler = () => handleSubmenuClose($menuItem);
const clickHandler = (e) => {
const $link = $menuItem.querySelector('a');
const $subMenu = $menuItem.querySelector('.sub-menu');
if (!$link || !$subMenu) return;
if (!$subMenu.classList.contains('open')) {
e.preventDefault();
$subMenu.classList.add('open');
}
// иначе — переход разрешён
};
submenuHandlers.set($menuItem, {
openHandler,
closeHandler,
clickHandler,
});
}
const { openHandler, closeHandler, clickHandler } = submenuHandlers.get($menuItem);
$menuItem.removeEventListener('mouseover', openHandler);
$menuItem.removeEventListener('mouseout', closeHandler);
$menuItem.removeEventListener('click', clickHandler);
if (screenWidth > 1023.5) {
$menuItem.addEventListener('mouseover', openHandler);
$menuItem.addEventListener('mouseout', closeHandler);
} else {
$menuItem.addEventListener('click', clickHandler);
}
});
};
const initializeEventListeners = () => {
$menuTriggers.forEach(($trigger) => {
$trigger.addEventListener('click', handleTriggerClick);
});
updateSubmenuEvents();
};
window.addEventListener('scroll', headerScroll);
window.addEventListener('resize', updateSubmenuEvents);
initializeEventListeners();
};
export default header;
Итог
- Простой случай:
Если функция-обработчик объявлена заранее и не создаётся динамически, достаточно сохранить её в переменной и использовать эту переменную как для добавления, так и для удаления слушателя. - Случай с Map:
Когда обработчик создаётся динамически для каждого элемента (или при изменении условий) и его нельзя просто переиспользовать, необходимо сохранить ссылку на него (например, в Map). Это позволяет корректно удалять слушатели, используя именно ту функцию, которая была установлена ранее.
