Базовый ультимативный гайд по событиям в JavaScript (на примере SvelteKit)
Published: 2023-11-14 19:10:21

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```html
<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>
```

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

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

## `e.stopPropagation`

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

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

```html
<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>
```

Выведет:
> `K1, K2`

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

Если убрать все `stopPropagation`, выведет:
> `К1, К2, К3, П1, П2, П3`

## `e.preventDefault`

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

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

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

Подробнее:
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`.
Но, если хочется выбрасывать событие, когда мы вышли за пределы элемента, то можно это сделать так:

```html
<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>
```

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

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

```html
<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>
```

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