DEV Community

Max Core
Max Core

Posted on • Edited on

Базовый ультимативный гайд по событиям в JavaScript (на примере SvelteKit)

addEventListener? removeEventListener? useCapture? capture?
click? mousedown? mouseup? touchstart? touchend?
touchcancel? dragleave?
stopPropogation? bubbling? capturing?
mouseenter? mouseleave? mouseover? mouseout?

Был такой случай

Выхожу из подъезда, передо мной бабушка.
Делает шаг в сторону со словами: «Молодым везде дорога!».
Я поблагодарил и вышел первым.
Думаю — «Ну наконец-то — хоть одно нормальное правило.» :D

Забавно, что, по-умолчанию — именно так и работают события в JavaScript.

По умолчанию — все действия Прогрессивные — сначала проходит Потомок, потом Родитель.

<div on:click={() => alert('Родитель проходит вторым')}>
    <button on:click={() => alert('Потомок проходит первым')}>
        Молодым везде дорога
    </button>
    <div>Да, согласна — молодым везде дорога</div>
</div>
Enter fullscreen mode Exit fullscreen mode

Есть только 1 способ начать пропускать старших:

Родитель должен высказать Консервативное мнение (capture):

<div on:click|capture={() => alert('Родитель проходит первым')}>
    <div>Нам — старикам — почёт в любом случае.</div>
    <button on:click={() => alert('Потомок проходит вторым')}>
        Молодым везде дорога же, не?
    </button>
</div>
Enter fullscreen mode Exit fullscreen mode

Если Родитель высказал Консервативное мнение — никакое Мнение Потомка не интересно.
В этом вся логика.

Логика событий

  1. Браузер всегда проводит 2 слушания по определённому событию (click в нашем случае): Сначала — все Консервативные мнения (capturing, события с capture). Потом — все Прогрессивные (bubbling, обычные события без capture).
  2. И, в каждом слушании начинает с мнения Родителя.

И, если Родитель высказал Консервативное мнение,
вот такое Консервативное мнение Потомка — не имеет смысла:

<div on:click|capture={() => alert('Родитель проходит первым')}>
    <div>Нам — старикам — почёт в любом случае.</div>
    <button on:click|capture={() => alert('Потомок проходит вторым')}>
        Да, согласен — старикам у нас почёт
    </button>
</div>
Enter fullscreen mode Exit fullscreen mode

Также, не имеет смысла Потомку высказывать Консервативное мнение если Родитель другого мнения:

<div on:click={() => alert('Родитель проходит вторым')}>
    <button on:click|capture={() => alert('Потомок проходит первым')}>
        Старикам у нас почёт
    </button>
    <div>Ты поучи жену щи варить, я принял Прогрессивное решение</div>
</div>
Enter fullscreen mode Exit fullscreen mode

Потому что, если Потомок выскажет Консервативное мнение — в этом также не будет смысла,
потому что у Родителя — нет Консервативного мнения.
Значит — он не хочет идти вперёд.

JavaScript это не жизнь. Здесь возможны 2-е параллельные вселенные:

<div on:click|capture={() => alert('C1')} on:click={() => alert('4')}>
    <button on:click|capture={() => alert('2')} on:click={() => alert('3')}>
        Кнопка
    </button>
</div>
Enter fullscreen mode Exit fullscreen mode

Именно поэтому, когда мы навешиваем:
addEventListener(process)
То удаляем мы его вот так:
removeEventListener(process)
А когда мы навешиваем:
addEventListener(process, true)
То и удаляем мы его вот так:
removeEventListener(process, true)

И, без разницы где и как именно указывать — true или {capture: true}.
К слову removeEventListener не будет работать, если событие навешено через html-атрибут (onclick="..."), даже если «перенавесить» через addEventListener.

Но, у нас есть не только click, есть и mousedown, mouseup и т.д.

Они тоже имеют свой порядок.
Сначала отработают вообще все mousedown на странице, потом mouseup, и только потом click.

<div
    on:mousedown|capture={() => console.log('D1')} on:mousedown={() => console.log('D4')}
    on:mouseup|capture={() => console.log('U1')} on:mouseup={() => console.log('U4')}
    on:click|capture={() => console.log('C1')} on:click={() => console.log('C4')}
>
    <button
        on:mousedown|capture={() => console.log('D2')} on:mousedown={() => console.log('D3')}
        on:mouseup|capture={() => console.log('U2')} on:mouseup={() => console.log('U3')}
        on:click|capture={() => console.log('C2')} on:click={() => console.log('C3')}
    >
        Кнопка
    </button>
</div>
Enter fullscreen mode Exit fullscreen mode

Выведет:
> D1, D2, D3, D4, U1, U2, U3, U4, C1, C2, C3, C4

Где посмотреть достоверный список приоритетов — не знаю.

e.stopPropagation

Без него можно обойтись.
В «моём кодстайле» его использовать запрещено в пользу игры с переменными is_el_doing_smth.
Но, во имя науки:

(П — Прогрессивное; К — Консервативное)

<div on:mouseup|capture={() => console.log('К1')}
     on:mouseup={() => console.log('П3')}>
    <div on:mouseup|capture|stopPropagation={() => console.log('К2')}
         on:mouseup|stopPropagation={() => console.log('П2')}>
        <div on:mouseup|capture={() => console.log('К3')}
             on:mouseup={() => console.log('П1')}>
            Поехали
        </div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Выведет:

K1, K2

Т.е. stopPropagation навешанный на событие с capture заблокировало все будущие mouseup — и с capture и без него.
Таким образом, что stopPropagation, навешанный на событие без capture уже не имеет смысла.
Однако, это никак не повлияет, если на эти же элементы навесить событие, которое вызывается позже, к примеру click.

Если убрать все stopPropagation, выведет:

К1, К2, К3, П1, П2, П3

e.preventDefault

Про него вы всё сами знаете, но, есть один момент:

<button
    on:touchstart={(e) => e.preventDefault()}
    on:click={() => alert('Алерт, который НЕ отработает на телефоне')}
>
    Кнопка 1
</button>

<button
    on:touchmove={(e) => e.preventDefault()}
    on:click={() => alert('Алерт, который отработает везде')}
>
    Кнопка 2
</button>
Enter fullscreen mode Exit fullscreen mode

Подробнее:
https://developer.mozilla.org/en-US/docs/Web/API/Touch_events

Чем отличаются mousedown, mouseup и click?

mousedown — нам не важно где был курсор когда мы нажали мышку, важно что отпустили его мы именно на этом элементе.
mouseup — нам не важно где мы отпустим мышку, главное, что нажата она была именно тут.
click — нажали, поводили хоть по всему экрану, вернулись на элемент, отпустили — и вот тогда это клик.

Т.е. click — мы могли бы реализовать сами.
Что, кстати, бывает очень нужно для реализации некого click-outside (будет в другой статье).
Потому что — пусть click — это mousedown и mouseup внутри одного элемента, он при этом не понимаем на каком именно элементе мы нажали, а на каком отпустили.
Есть конечно всякие e.target, e.currentTarget, e.relatedTarget, e.originalTarget и т.д.
Но, половина из них не работает для click, а вторая половина не везде поддерживается (всё это без меня).

Из того, что работает одинаково для всех событий:
e.currentTarget — это элемент на котором висит само событие, а
e.target — это первый самый далёко-вложенных Потомок внутри этого элемента.

click работает и на компе и на телефоне, т.е. никакого события touch на телефоне не существует.

Но, на телефоне:
mousedown — это — touchstart
mouseup — это — touchend
К слову:
mousemove — это — touchmove

Есть ещё mouseleave, когда курсор покидает элемент.
Но на телефоне — нет курсора.
Однако, на телефоне, мы можем нажать и вести, и тогда mouseleave мог бы пригодиться.
Но, это событие решили не реализовывать.
Вместо этого, сделали так, что когда мы ведём пальцем за пределы экрана (не браузера) — выбрасывается touchend.
Но, если хочется выбрасывать событие, когда мы вышли за пределы элемента, то можно это сделать так:

<script>
    let el;
</script>
<div style="height:100px;background:#f00;"
     on:touchstart={(e) => el = e.currentTarget}
     on:touchmove={(e) => {
         const {pageX, pageY} = e.touches[0];
         const realElement = document.elementFromPoint(pageX, pageY)
         if (realElement !== el) {
             alert('touchleave')
         }
     }}>
</div>
Enter fullscreen mode Exit fullscreen mode

Есть ещё touchcancel, призванный обработать конфликты нажатия несколькими пальцами, но не работает в Safari, пэтому не используем.
Есть ещё dragleave, но это не ведение пальце, а именно перетаскивание элемента (картинки).

К слову, чем отличается mouseenter+mouseleave от mouseover+mouseout:

<div data-name="Дом" style="height:100px;background:#f00;"
     on:mouseenter={(e) => {console.log('вОшли Дом')}}
     on:mouseleave={(e) => {console.log('вЫшли Дома')}}
     on:mouseover={(e) => {console.log('перешли В Дом или Комнату')}}
     on:mouseout={(e) => {console.log('перешли ИЗ Дома или Комнаты')}}
>
    Каждый раз когда мы заходим в Дом — вызывается `mouseenter`.<br>
    Каждый раз когда мы выходим из Дома — вызывается `mouseleave`.
    <pre data-name="Комната" style="height:100px;width:50%;margin-left:50%;background:#000;">
        Каждый раз когда курсор пересекает границу этой Комнаты внутри Дома:<br>
            вызываются и mouseout и mouseover (именно в таком порядке).
    </pre>
</div>
Enter fullscreen mode Exit fullscreen mode

Иии, внииимаааниииеее...

Спасибо, за внимание.

Top comments (0)