В предыдущей главе вы узнали, как использовать функцию addEventListener для прослушивания событий, на которые нужно среагировать. Но кроме основ мы также затронули важную тему того, как именно события срабатывают. Событие — это не обособленное возмущение. Подобно бабочке, машущей крыльями, землетрясению, падению метеорита или визиту Годзиллы, многие события расходятся волнами и воздействуют на другие элементы, которые лежат у них на пути.
Сейчас я превращусь в Шерлока Холмса и раскрою вам тайну, что же именно происходит при срабатывании события. Вы узнаете о двух фазах, в которых пребывают события, поймете, почему это важно знать, а также познакомитесь с некоторыми трюками, которые помогут лучше контролировать события.
Событие опускается. Событие поднимается
Для наглядности оформим все в виде простого примера:
Вроде бы ничего особенного здесь не происходит. HTML должен выглядеть достаточно понятно, и его представление DOM приведено на рис. 32.1.
Отсюда и начнем наше расследование. Давайте представим, что щелкаем по элементу buttonOne. Исходя из пройденного материала, вы знаете, что при этом запустится событие клика. Интересная же часть, которую до этого я опускал, заключается в том, откуда конкретно будет запущено это событие. Оно (как почти каждое событие в JavaScript) не возникает в элементе, с которым произошло взаимодействие. Иначе все бы было слишком просто и логично.
Рис. 32.1. Так выглядит DOM для разметки, приведенной выше
Вместо этого событие стартует из корня вашего документа:
Начиная с корня, событие проделывает путь по узким тропкам DOM и останавливается у элемента, который его вызвал, а именно buttonOne (также известного как целевое событие):
Как показано на рисунке, событие совершает прямой путь, но при этом наглым образом уведомляет каждый элемент на своем пути. Это означает, что если бы вы прослушивали событие клика в body, one_a, two или three_a, то сработал бы связанный с ними обработчик событий. Это важная деталь, к которой мы еще вернемся.
Как только наше событие достигнет своей цели, оно не остановится и, как кролик из известной рекламы батареек, продолжает движение по своим следам обратно к корню:
Как и прежде, каждый элемент на пути события будет уведомлен о его присутствии.
Знакомьтесь с фазами
Важно заметить, что не имеет значения, где в DOM инициируется событие. Оно всегда начинает движение от корня, спускается вниз до встречи с целью, а затем возвращается к корню. Этот путь имеет официальное определение, поэтому давайте рассмотрим его с этой позиции.
Часть, в которой вы инициируете событие и оно, начиная с корня, совершает спуск вниз по DOM, называется фазой погружения события.
Менее продвинутые люди иногда называют его фаза 1, так что имейте в виду, что в реальной жизни вы будет встречать верное название вперемешку с названием фазы. Следующей будет фаза 2, во время которой событие всплывает обратно к корню.
Эта фаза также известна как фаза всплытия события.
Как бы то ни было, но всем элементам на пути события в некотором смысле повезло. Судьба наградила их возможностью двойного уведомления при срабатывании события. Это может повлиять на код, который вы пишете, так как каждый раз, когда мы прослушиваем события, решаем, в какой фазе нужно это делать. Слушаем ли мы событие во время его спуска в фазе погружения или же тогда, когда оно взбирается обратно в фазе всплытия?
Выбор фазы — это тонкая деталь, которую вы определяете с помощью true или false в вызове addEventListener:
item.addEventListener("click", doSomething, true);
Если вы помните, в предыдущей главе я вскользь упомянул третий аргумент — addEventListener. Этот аргумент указывает, хотите ли вы прослушивать событие во время фазы погружения. В этом смысле значение true означает, что именно так вы и хотите. И наоборот, аргумент false будет означать, что нужно прослушивать событие во время фазы всплытия.
Чтобы прослушивать его в обеих фазах, можно сделать следующее:
item.addEventListener("click", doSomething, true);
item.addEventListener("click", doSomething, false);
Я не могу представить, зачем вам это может понадобиться, но если вдруг понадобится, вы знаете, что делать.
не указана фаза
Можно возмутиться и вообще не указывать этот третий аргумент для фазы:
item.addEventListener("click", doSomething);
Если вы не укажете третий аргумент, то поведением по умолчанию будет прослушивание вашего события во время фазы восходящей цепочки. Это эквивалентно передаче в качестве аргумента ложного значения.
Кому это важно?
Вы можете спросить: «А почему это все важно?» Такой вопрос вдвойне справедлив, если вы и так давно работали с событиями и только сейчас обо всем этом прочитали. Выбор в пользу прослушивания события во время погружения или всплытия по большей части не зависит от того, что вы делаете. Очень редко может возникнуть путаница, когда код, отвечающий за прослушивание и обработку событий, делает не то, что нужно, так как вы случайно указали true вместо false в вызове addEventListener.
Этим я лишь хочу сказать, что в жизни может возникнуть ситуация, когда потребуется разбираться в фазах погружения/всплытия и работать с ними. Ошибка прокрадется в ваш код и выльется в многочасовое чесание затылка в поисках решения. Я могу привести список ситуаций из своей практики, когда мне пришлось осознанно выбирать фазу, в которой я наблюдал за событиями:
1. Перетаскивание элемента по экрану и обеспечение продолжения перетаскивания, даже если перемещаемый элемент выскользнет из-под курсора.
2. Вложенные меню, открывающие подменю при наведении на них указателя.
3. Есть несколько обработчиков событий в обеих фазах, а вы хотите сфокусироваться только на обработчиках в фазе погружения или всплытия.
4. Сторонний компонент и библиотека элементов управления имеют свою логику событий, и вы хотите обойти ее, чтобы использовать собственную настройку поведения.
5. Вы хотите переназначить предустановленное поведение браузера, например, когда вы нажимаете на полосу прокрутки или переключаетесь на текстовое поле.
За мои уже почти 105 лет работы с JavaScript могу привести только такие примеры. И даже они уже не столь однозначны, поскольку некоторые браузеры вообще некорректно работают с различными фазами.
Прерывание события
Последнее, о чем поговорим, — это о предотвращении распространения события. У события не обязательно должна быть полноценная жизнь, в которой оно начинается и заканчивается в корне. Бывает, что лучше не давать ему счастливо дожить до старости.
Чтобы прекратить существование события, можно использовать метод stopPropagation в объекте Event:
function handleClick(e) {
e. stopPropagation();
// что-нибудь делает
}
Метод stopPropagation прекращает движение события по фазам. Обратившись к предыдущему примеру, давайте предположим, что вы прослушиваете событие click в элементе three_a и хотите помешать этому событию распространиться. В этом случае код будет выглядеть так:
let theElement = document.querySelector("#three_a");
theElement.addEventListener("click", doSomething, true);
function doSomething(e) {
e. stopPropagation();
}
В данном случае при нажатии на buttonOne путь нашего события будет выглядеть так:
Событие click начнет быстрое движение вниз по дереву DOM, уведомляя каждый элемент на своем пути к buttonOne. Так как элемент three_a прослушивает событие click во время фазы погружения, будет вызван связанный с ним обработчик событий:
function doSomething(e) {
e. stopPropagation();
}
Как правило, события не продолжают распространение, пока взаимодействие с активированным обработчиком событий не будет завершено. Поскольку обработчик событий для three_a настроен реагировать на событие click, происходит вызов обработчика событий doSomething. Событие попадает в состояние задержки до тех пор, пока обработчик событий doSomething не будет выполнен и возвращен.
В данном случае событие не будет распространяться. Обработчик событий doSomething оказывается его последним клиентом благодаря функции stopPropagation, которая притаилась в тени, чтобы разделаться с событием раз и навсегда. Событие click не достигнет элемента buttonOne и не получит возможности вернуться к корню, как бы печально это ни было.
СОВЕТ
В вашем объекте события существует еще одна функция, с которой вы можете ненароком встретиться, и называется она preventDefault:
function overrideScrollBehavior(e) {
e. preventDefault();
// делает что-нибудь
}
Действия этой функции немного загадочны. Многие HTML-элементы при взаимодействии с ними демонстрируют стандартное поведение. Например, щелчок по текстовой рамке производит переключение на нее и вызывает появление мигающего курсора. Использование колесика мыши в области, допускающей прокрутку, приведет к прокрутке в соответствующем направлении. Щелчок в графе для галочки переключит состояние отметки в положение да/нет. Браузер по умолчанию знает, как обработать встроенные реакции на все приведенные события.
Если нужно отключить это встроенное поведение, можно вызвать функцию preventDefault. Ее нужно вызывать во время реагирования на событие в элементе, чью встроенную реакцию вы хотите проигнорировать. Мой пример применения этой функции можно посмотреть здесь: http://bit.ly/kirupaParallax.
КОРОТКО О ГЛАВНОМ
Ну и как вам эта тема про события с их погружением и всплытием? Лучшим способом освоить принципы работы погружения и всплытия событий будет написание кода и наблюдение за перемещением события по DOM.
На этом мы завершили техническую часть этой темы, но если у вас есть несколько свободных минут, я предлагаю вам посмотреть связанный с ней эпизод Comedians in Cars Getting Coffee, метко названный It’s Bubble Time, Jerry!. Это, вероятно, их лучший эпизод, в котором Майкл Ричардс и Джерри Сайнфелд попивают кофе и беседуют о событиях, фазе всплытия и прочих, на мой взгляд, важных вещах.