События в DOM
Вопрос
Как событие идёт по дереву и зачем вешать один слушатель на родителя?
Короткий ответ
Событие идёт в три фазы: захват от window к цели, сама цель, всплытие обратно к window. addEventListener по умолчанию слушает всплытие. Третий аргумент true или { capture: true } включает захват.
Делегирование: один слушатель на родителе ловит клики детей, потому что событие всплывает. В обработчике смотрят event.target — фактический узел, по которому кликнули. Так список не вешает слушатель на каждую строку, и новые строки работают без повторной подписки.
preventDefault() отменяет действие браузера: переход по ссылке, отправку формы. Всплытие при этом продолжается. stopPropagation() останавливает ход по дереву и не отменяет действие браузера. Это разные вещи.
Разбор
target — узел, где событие возникло. currentTarget — узел, на котором висит текущий слушатель. При делегировании они различаются.
Не все события всплывают. focus, blur, mouseenter, mouseleave не всплывают. Для делегирования фокуса берут focusin / focusout или захват.
{ once: true } снимет слушатель после первого вызова. { passive: true } обещает не вызывать preventDefault. Браузер тогда не ждёт обработчик, чтобы решить, скроллить ли. На touchmove и wheel пассивный слушатель по умолчанию в современных браузерах на корне документа.
На что смотрят на senior
Делегирование на document ловит всё, но перемешивает чужие виджеты. Слушатель ближе к списку проще снять при размонтировании. В Vue слушатель вешают не на window без onUnmounted: тот же класс утечек, что у любого SPA.
stopPropagation в общем компоненте ломает аналитику и внешний click-outside, которые слушают документ. Отменяют действие браузера, всплытие режут только с явной причиной.
Типичные ошибки
- Путать
preventDefaultиstopPropagation. - Делегировать
focus, который не всплывает. - Читать
thisвнутри стрелочного обработчика и ждать узел. У стрелки нет своегоthis, узел берётся изcurrentTarget. - Вешать слушатель на каждую строку длинного списка.
Задача
На ul висит один слушатель клика. По клику на button внутри li нужно удалить эту li. Откуда брать узел и чем preventDefault тут не поможет?
Показать ответ
Узел берут из event.target, затем closest('li'). currentTarget — это сам ul, не строка.
preventDefault отменяет действие браузера, например переход по ссылке. Удаление строки — это уже ваш код, его preventDefault не делает. stopPropagation тоже не удаляет узел, оно только режет всплытие.