Что такое пропагация?

Пропогация события — от английского «распространение» — это процесс возникновения, перемещения и обработки события внутри DOM.

Это одно из фундаментальных свойств поведения Document Object Model, зная которое, вы сможете лучше манипулировать поведением события.

Фазы распространения

Распространение состоит из двух фаз (строго говоря из трех, но фазу достижения таргета для упрощения будем считать частью фазы всплытия).

Распространение события начинается с объекта окна (window). Потом собитие переходит к документу (document), затем к телу (document.body) и так к элементу, который вызвал событие (target). Путь от объекта окна до элемента триггера — это первый этап распространения, которое называется погружение (capturing).

Затем начинается фаза всплытия (bubbling), на которой как раз и происходит «дефолтное» слушание событий документом.

Наглядно это можно видеть на схеме:

Таким образом, когда событие отработает на таргет‑элементе, оно не «закончиться», а будет всплывать вверх, и соответственно, на всех родительских объектах, которые слушают событие этого типа, это событие тоже сработает.

Такое поведение является стандартным, и чтобы остановить дальнейшее распространение события, можно в одном из обработчиков вызвать метод stopPropagation. Тогда событие «закончиться» и распространяться дальше не будет.

Давайте посмотрим, когда нам может это пригодиться.

Допустим мы делаем реакт‑приложение и решили воспользоваться внешней библиотекой компонентов. Доступа к исходному коду у нас нет, и соответственно — использовать компоненты библиотеки можете только с предоставленным интерфейсом.

Импортированная форма содержит инпут, кнопку сабмита и имеет поля для передачи обработчиков событий ввода и сабмита, соответственно. Обернем форму во враппер, с помощью которого, мы будем манипулировать стилем и поведением формы:

const FormWrapper = () => {
  return (
    <div className={`form-wrapper`}>
      <Form
        onChange={() => console.log('Something typed...')}
        onSubmit={() => console.log('Ready to submit!')}
      />
    </div>
  );
};

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

const FormWrapper = () => {
  const [wrapperElem, setWrapperElem] = useState(null);
  const [isBlocked, setIsBlocked] = useState(true);

  useElementOutsideClick(wrapperElem, () => setIsBlocked(_isBlocked => !_isBlocked));

  return (
    <div
      ref={setWrapperElem}
      className={`form-wrapper ${isBlocked ? 'blocked' : ''}`}
    >
      <Form
        onChange={() => console.log('Something typed...')}
        onSubmit={() => console.log('Ready to submit!')}
      />
    </div>
  );
};

Пока форма будет заблокирована, мы хотим две вещи: во первых — чтобы у заблокированных элементов формы был свой стиль, но при этом сохранились дефолтные стили их псевдособытий — в частности, скажем, нам очень нравиться анимация при ховере на кнопке, и мы хотим ее сохранить. А во вторых — чтобы обработчики событий этих элементов не срабатывали в состоянии блокировки. Готового интерфейса блокировки форма не предоставляет.

Как решать эту задачу? Давайте разбираться по порядку.

Чтобы применить форме стиль, мы можем присвоить его в css родительского компонента, зацепившись за класс‑селектор формы.

А вот чтобы заблокировать события на блокировке, есть два стандартных варианта: стилями при помощи css‑параметра pointer‑events, или скриптом не передавать обработчик в импортированную форму.

Блокировать события через pointer‑events не подходит, так как в этом случае также заблокируются стили псевдособытий. Значит проблему нужно решать скриптом.

Можно добавить условие, по которому в компонент не будут передаваться обработчики событий, когда форма заблокирована. Проблема решена. Но, вдруг, в форме нет проверки на отсутствие обработчика, и тогда вызов undefined сломает приложение. Конечно, в таком случае можно передавать обработчик‑пустышку, но предположим что перед выполнением переданного обработчика, в обработчике самой формы выполняются какие то действия, которые мы тоже не хотим выполнять.

В таком случае лучше воспользоваться фазой захвата на враппере формы и полностью остановить дальнейшую передачу события в компонент формы.

Для этого можно воспользоваться специальным атрибутом компонента — onClickCapture. Интерфейс реакта предоставляет каждому методу события его собрата в фазе захвата — с таким же именем и постфиксом Capture. В нативном джаваскрипте, чтобы указать, что обработчик должен выполняться в фазе захвата, в addEventListene, добавляется опция { capture: true }.

Добавим нашему враперу слушатель события фазы захывата и заблокируем в нем дальнейшее распространение события методом preventPropagation:

const FormWrapper = () => {
  const [wrapperElem, setWrapperElem] = useState(null);
  const [isBlocked, setIsBlocked] = useState(true);

  useElementOutsideClick(wrapperElem, () => setIsBlocked(_isBlocked => !_isBlocked));

  return (
    <div
      ref={setWrapperElem}
      className={`form-wrapper ${isBlocked ? 'blocked' : ''}`}
      // Prevent child clicks
      onClickCapture={ev => isBlocked && ev.stopPropagation()}
    >
      <Form
        onChange={() => console.log('Something typed...')}
        onSubmit={() => console.log('Ready to submit!')}
      />
    </div>
  );
};

Готово! Дальнейшая передача события полностью остановлена, и при этом мы никак не повлияли на стили псевдособытий.

В целом, на этом примере, поверхностно, но наглядно можно увидеть принцип работы распространения события.

По теме еще хочется добавить про разницу атрибутов события target и currentTarget. Разница между ними в том, что target ссылается на элемент вызвавший событие, а currentTarget - тот элемент, который в обработчике его поймал - и это могут быть два разных элемента.

Также на learn.javascript.ru есть отличная статья про фазы погружения\всплытия.

спасибо

Комментарии (0)