W3docs

JavaScript Перемещение мыши: mouseover/out, mouseenter/leave

Разница между mouseover/mouseout и mouseenter/mouseleave в JavaScript: всплытие, relatedTarget, особенности дочерних элементов и примеры кода.

Понимание событий перемещения мыши в JavaScript: Mouseover, Mouseout, Mouseenter и Mouseleave

События перемещения мыши events в JavaScript дают разработчикам возможность реагировать на движение курсора над элементами веб-страницы. Эти события необходимы для создания интерактивных и отзывчивых интерфейсов, которые реагируют на действия пользователя. В этом руководстве мы рассмотрим различия между событиями mouseover, mouseout, mouseenter и mouseleave и приведём практические примеры их использования.

Две пары событий в сравнении

JavaScript предоставляет две пары событий для одного и того же физического действия — перемещения указателя на элемент и с элемента. На первый взгляд они взаимозаменяемы, но ведут себя очень по-разному при наличии дочерних элементов, и выбор неправильной пары является одной из наиболее распространённых ошибок в UI.

СобытиеСрабатывает когда указатель…Всплывает?Повторно срабатывает на дочерних элементах?
mouseoverвходит в элемент или любой его потомокДаДа
mouseoutпокидает элемент или любой его потомокДаДа
mouseenterпересекает границу самого элементаНетНет
mouseleaveпокидает границу самого элементаНетНет

Mouseover и Mouseout

  • mouseover: Срабатывает, когда мышь входит в элемент или любой из его дочерних элементов. Поскольку событие всплывает, оно является правильным выбором для делегирования событий — один обработчик на контейнере может управлять наведением на множество дочерних элементов.
  • mouseout: Зеркальное отражение mouseover — срабатывает, когда мышь покидает элемент или любой из его дочерних элементов.

Особенность в том, что при перемещении указателя с родительского элемента на дочерний срабатывает mouseout для родителя (указатель «покинул» текстовую область родителя) сразу же за ним следует mouseover (он «вошёл» в дочерний элемент, который по-прежнему считается частью родителя). Таким образом, одно визуальное наведение может порождать шумный поток событий для родительского элемента с дочерними элементами.

Mouseenter и Mouseleave

  • mouseenter: Похоже на mouseover, но событие не всплывает и не срабатывает повторно при пересечении указателем границы дочернего элемента. Оно срабатывает ровно один раз, когда указатель впервые входит в границу элемента — идеально для поведения «выделить карточку при наведении».
  • mouseleave: Срабатывает один раз, когда указатель покидает внешнюю границу элемента, игнорируя перемещения между потомками.

Практическое правило: используйте mouseenter/mouseleave, когда нужна чистая логика «находится ли указатель над этим элементом?», и mouseover/mouseout только тогда, когда вам нужно всплытие для делегирования событий.

Свойство relatedTarget

Оба события предоставляют свойство event.relatedTarget, которое указывает на другой элемент, участвующий в переходе:

  • При mouseover/mouseenter свойство relatedTarget содержит элемент, с которого пришёл указатель.
  • При mouseout/mouseleave свойство relatedTarget содержит элемент, на который перемещается указатель.

Именно так воспроизводится поведение mouseenter при использовании всплывающих событий mouseover/mouseout: нужно проверить, находится ли relatedTarget внутри текущего элемента, и игнорировать событие, если это так.

element.addEventListener('mouseout', function (event) {
  // Ignore transitions to a descendant — only react to truly leaving.
  if (this.contains(event.relatedTarget)) return;
  console.log('Pointer really left the element');
});

Обратите внимание, что relatedTarget может быть null — например, когда указатель приходит из-за пределов окна браузера — поэтому перед вызовом contains() нужно проверять это значение.

Особенность: «быстрое движение мыши»

События mouseover/mouseout не гарантированно срабатывают для каждого элемента, над которым проходит указатель. Если пользователь перемещает мышь очень быстро, промежуточные элементы могут быть полностью пропущены, и можно получить mouseout для одного элемента без соответствующего mouseover для следующего. Код, который использует оба события в паре, должен быть рассчитан на отсутствие одного из них. События mouseenter/mouseleave всегда сбалансированы для элемента, к которому они привязаны, что является ещё одной причиной предпочитать их для отслеживания состояния.

Практические примеры событий перемещения мыши

Эти примеры демонстрируют, как реализовать события перемещения мыши для улучшения пользовательского опыта с помощью интерактивных элементов.

Пример 1: Использование Mouseover и Mouseout

Этот пример показывает, как изменить цвет фона блока при входе и выходе курсора мыши, включая его дочерние элементы.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Mouseover and Mouseout Example</title>
    <style>
        #box {
            width: 200px;
            height: 200px;
            background-color: lightblue;
        }
        #innerBox {
            width: 100px;
            height: 100px;
            background-color: lightcoral;
            margin: 50px;
        }
    </style>
</head>
<body>
<div id="box">
    Hover over me!
    <div id="innerBox"></div>
</div>

<script>
    document.getElementById('box').addEventListener('mouseover', function() {
        this.style.backgroundColor = 'cyan';
    });
    document.getElementById('box').addEventListener('mouseout', function() {
        this.style.backgroundColor = 'lightblue';
    });
</script>
</body>
</html>

Пояснение:

  • Событие mouseover изменяет цвет фона блока на голубой, в том числе при наведении на внутренний блок.
  • Событие mouseout сбрасывает цвет фона, когда мышь покидает блок, с учётом внутреннего блока.

Пример 2: Использование Mouseenter и Mouseleave

Этот пример улучшает взаимодействие с пользователем, демонстрируя применение mouseenter и mouseleave для более точной реакции без влияния на дочерние элементы.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Mouseenter and Mouseleave Visual Example</title>
    <style>
        #parent {
            width: 400px;
            height: 300px;
            background-color: lightblue; /* Initial background color */
            padding: 20px;
            box-sizing: border-box;
            position: relative;
            display: flex;
            justify-content: space-around;
            align-items: center;
            transition: background-color 0.3s ease;
        }
        .child {
            width: 90px;
            height: 90px;
            background-color: lightsalmon;
            display: flex;
            justify-content: center;
            align-items: center;
            transition: background-color 0.3s ease;
        }
        #feedback {
            position: fixed;
            bottom: 10px;
            left: 10px;
            background: white;
            padding: 10px;
            border: 1px solid #ccc;
            font-family: Arial, sans-serif;
        }
    </style>
</head>
<body>
<div id="parent">
    Parent Element
    <div class="child">Child 1</div>
    <div class="child">Child 2</div>
    <div class="child">Child 3</div>
</div>
<div id="feedback">Hover over elements to see interactions.</div>

<script>
    const parent = document.getElementById('parent');
    const children = document.querySelectorAll('.child');
    const feedback = document.getElementById('feedback');

    parent.addEventListener('mouseenter', function() {
        parent.style.backgroundColor = 'cyan'; // Highlight the parent on mouse enter
        feedback.textContent = 'Mouse entered the parent element';
    });

    parent.addEventListener('mouseleave', function() {
        parent.style.backgroundColor = 'lightblue'; // Revert color on mouse leave
        feedback.textContent = 'Mouse left the parent element';
    });

    // Update feedback for child interactions
    children.forEach(child => {
        child.addEventListener('mouseenter', function() {
            feedback.textContent = `Mouse entered ${this.textContent}`;
            this.style.backgroundColor = '#ffcccb'; // Highlight child on mouse enter
        });
        child.addEventListener('mouseleave', function() {
            feedback.textContent = `Mouse left ${this.textContent}`;
            this.style.backgroundColor = 'lightsalmon'; // Revert child color on mouse leave
        });
    });
</script>
</body>
</html>

Этот пример наглядно демонстрирует, что события mouseenter и mouseleave срабатывают точечно и не всплывают, обеспечивая отдельные и изолированные взаимодействия с вложенными элементами.

Пример 3: Имитация mouseleave с помощью relatedTarget

Иногда нужно одновременно использовать всплытие (чтобы применить единственный делегированный обработчик) и чистое поведение «только когда указатель действительно покидает элемент». Это можно совместить, прослушивая всплывающий mouseout и игнорируя переходы к потомкам с помощью relatedTarget. Логика та же, что показана выше, оформленная в виде небольшого переиспользуемого вспомогательного метода:

function reallyLeft(event, element) {
  // True only when the pointer moves to something OUTSIDE `element`.
  const to = event.relatedTarget;
  return to === null || !element.contains(to);
}

// Demonstrate without a browser: simulate a mouseout whose related
// target is a child (should be ignored) and one to an outside node.
const card = { contains: (node) => node === 'child' };

console.log(reallyLeft({ relatedTarget: 'child' }, card));   // false (still inside)
console.log(reallyLeft({ relatedTarget: 'outside' }, card)); // true  (really left)
console.log(reallyLeft({ relatedTarget: null }, card));      // true  (left the window)

Этот паттерн лежит в основе реализации надёжных выпадающих меню в библиотеках: один всплывающий обработчик размещается на корне меню, а меню сворачивается только тогда, когда указатель покидает всё поддерево.

Заключение

События перемещения мыши позволяют создавать тонкие и отзывчивые взаимодействия вокруг указателя пользователя. Главный вывод — правильный выбор пары:

  • Используйте mouseenter/mouseleave для чистого отслеживания состояния наведения для каждого элемента — они срабатывают один раз и игнорируют дочерние элементы.
  • Используйте mouseover/mouseout когда вам нужно всплытие для делегирования событий, и опирайтесь на relatedTarget для фильтрации переходов к потомкам.

Для дальнейшего изучения ознакомьтесь с основами событий мыши для кликов и кнопок, а также с введением в события браузера для общего понимания того, как работают события.

Практика

Практика
Каковы ключевые различия между событиями Mouseover/Mouseout и Mouseenter/Mouseleave в JavaScript?
Каковы ключевые различия между событиями Mouseover/Mouseout и Mouseenter/Mouseleave в JavaScript?
Was this page helpful?