ЧАСТЬ IV. Полны событиями

Глава 31. События

Многие приложения и веб-сайты, оставленные без внимания, становятся очень скучными. Запускаются они с шиком и помпой, но вся эта напыщенность очень быстро пропадает, если мы перестаем взаимодействовать с ними:

Причина проста. Приложения существуют, чтобы реагировать на наши в них действия. Хоть они и имеют определенную базовую мотивацию, проявляющуюся при запуске, которая вытаскивает их из постели и приводит в рабочее состояние, все дальнейшие их действия уже по большей части диктуются нами. Тут все и становится интересно.

Чтобы сообщить приложениям, что нужно делать, мы используем так называемые события, на которые они в итоге реагируют. В этой главе узнаем, чем являются эти события и как их можно использовать.

Поехали!

Что такое события?

На высоком уровне все создаваемое нами может быть смоделировано следующей инструкцией:

Когда___ совершится, то____

Мы можем заполнить пробелы этой инструкции великим множеством способов. Первый пробел описывает то, что происходит, а второй уже описывает реакцию на это. Вот некоторые примеры с заполненными пробелами:

Когда страница будет загружена, начнется воспроизведение видео с кошкой, скользящей на картоне.

Когда произойдет щелчок, покупка будет отправлена.

Когда ЛКМ будет освобождена, вылетит гигантская / не очень довольная птичка.

Когда клавиша удаления будет нажата, файл отправится в корзину.

Когда произойдет касание, к фотографии будет применен старый фильтр.

Когда файл будет загружен, обновится индикатор выполнения.

Эта обобщенная модель применима ко всему коду, который мы с вами написали. Она также применима ко всему коду, который написали для своих приложений наши друзья-разработчики/дизайнеры. От этой модели никуда не деться, поэтому и смысла противиться ей нет. Наоборот, следует хорошенько познакомиться с важным и очень талантливым представителем этой модели — событием.

Событие — это не более чем сигнал. Оно сообщает о том, что что-то только что произошло. Это что-то может быть щелчком мыши, нажатием клавиши клавиатуры, изменением размера окна или загрузкой документа. Главное здесь — понять, что этим сигналом могут послужить сотни встроенных в JavaScript явлений, равно как бессчетное множество отдельных пользовательских решений, созданных под конкретное приложение.

Вернемся к нашей модели, где события составляют первую ее половину:

Событие определяет то, что происходит, и передает сигнал. Вторая часть модели определяет реакцию на это событие:

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

События и JavaScript

Учитывая важность событий, вас не должно удивлять, что JavaScript дает богатые возможности для работы с ними. Всего для их использования нам понадобится делать две вещи:

1. Прослушивать события.

2. Реагировать на события.

Оба этих шага кажутся простыми, но не стоит забывать, что мы имеем дело с JavaScript. Простота в этом случае является всего лишь завесой, скрывающей печальные последствия, которые JS обрушит на нас, сделай мы один неверный шаг. Но возможно, я драматизирую.

1. Прослушивание событий

Практически все, что мы делаем внутри приложения, заканчивается срабатыванием событий. Иногда наше приложение будет запускать события автоматически, например при своей загрузке. В других случаях оно будет запускать их в виде реакции на наши действия. Здесь стоит отметить, что наше приложение постоянно бомбардируется событиями независимо от того, хотим мы, чтобы они срабатывали, или нет. Наша же задача — указать приложению прослушивать только те события, которые нас интересуют.

Неблагодарная работа по прослушиванию нужных событий полностью обрабатывается функцией, называемой addEventListener. Эта функция всегда начеку, поэтому может в нужное время уведомить другую часть приложения о том, что произошло некоторое событие.

Используется она следующим образом:

source.addEventListener(eventName, eventHandler, false);

Наверняка так не совсем понятно, поэтому давайте разберем, что именно означает каждая часть этой функции.

Source (источник)

Мы вызываем addEventListener через элемент или объект, в котором мы хотим прослушивать события. Как правило, это будет элемент DOM, но им также может быть document, window или другой объект, специально созданный для запуска событий.

Event Name (имя события)

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

Табл. 31.1. Распространенные события

Событие

Событие срабатывает…

Click

…когда вы нажимаете и отпускаете основную кнопку мыши, сенсорной панели и т. п.

Mousemove

…при движении курсора мыши

Mouseover

…при наведении курсора мыши на элемент. Это событие используется для обнаружения наведения

Mouseout

…при перемещении курсора мыши за границы элемента

Dblclick

…при быстром двойном клике

DOMContentLoaded

…когда DOM документа полностью загрузилась. Подробнее об этом событии в следующей главе

Load

…когда загрузился весь документ (DOM, внешние изображения, сценарии и т. п.)

Keydown

…при нажатии клавиши клавиатуры

Keyup

…при прекращении нажатия клавиши клавиатуры

Scroll

…при прокрутке элемента

wheel and DOMMouseScroll

…при использовании колесика мыши для прокрутки

В последующих главах мы рассмотрим многие из этих событий более подробно. На данный момент просто взгляните на событие click. Именно его мы будет использовать в скором времени.

Обработчик событий

Во втором аргументе нужно указать функцию, которая будет вызвана, когда событие будет услышано. Эта функция (а иногда объект) в кругу семьи и друзей зовется обработчик событий. Через несколько мгновений мы узнаем о ней гораздо больше.

Захватывать или не захватывать, вот в чем вопрос!

Последний аргумент состоит из true либо false. Чтобы полноценно понять последствия определения того или иного значения, нам придется подождать до главы «Всплытие и погружение событий», которая как раз будет следующей.

Обобщая все сказанное

Теперь, когда мы разобрали функцию addEventListener, давайте все это свяжем в полноценном заполненном примере:

document.addEventListener("click", changeColor, false);

В этом примере addEventListener прикреплена к объекту document. Когда будет услышано событие click, она вызовет функцию changeColor (то есть обработчик событий), которая отреагирует на это событие. Теперь самое время перейти к следующему разделу, посвященному как раз реагированию на события.

2. Реагирование на события

Как мы видели в предыдущем разделе, за прослушивание событий отвечает addEventListener. За действия, производимые после того, как событие будет услышано, отвечает обработчик событий. И я не шутил про то, что обработчик событий — это не более чем функция или объект:

function normalAndBoring() {

// я люблю походы, щенков и многие другие вещи!

}

Единственным отличием между типичной функцией и установленной мной в качестве обработчика событий в том, что функция обработчика событий специально вызывается по имени в вызове addEventListener (и получает объект Event в виде аргумента):

document.addEventListener("click", changeColor, false);


function changeColor(event) {

// I am important!!!

}

Любой код, который мы поместим в обработчик событий, будет выполнен, когда событие, за которым следит наша функция addEventListener, будет услышано.

Простой пример

Лучшим способом разобраться в этом материале будет рассмотрение его в действии. Чтобы попутно проработать пример самим, добавьте в HTML-документ следующую разметку и код:


Click Anywhere!



Если мы просмотрим наш документ в браузере, то изначально увидим пустую страницу (рис. 31.1).

Но все изменится, если щелкнуть в любом месте этой страницы. По завершении щелчка (то есть как только вы отпустите кнопку мыши) цвет фона страницы изменится на желтый (рис. 31.2).

Причина в коде:

document.addEventListener("click", changeColor, false);


function changeColor() {

document.body.style.backgroundColor = "#FFC926";

}

Вызов addEventListener идентичен тому, что мы уже видели, поэтому его можно пропустить. Вместо этого давайте обратим внимание на обработчика событий changeColor.

document.addEventListener("click", changeColor, false);


function changeColor() {

document.body.style.backgroundColor = "#FFC926";

}

Рис. 31.1. Просто пустая страница

Рис. 31.2. Страница становится желтой после щелчка

Эта функция вызывается, как только событие click в document будет услышано. Когда происходит вызов этой функции, она устанавливает фоновый цвет как желтый. Если связать это с самым началом, где мы в общих чертах обсуждали принцип работы приложений, то вот как будет выглядеть наш пример:

Надеюсь, что теперь все стало понятно. Вы только что изучили один из важнейших принципов работы, с которым предстоит сталкиваться постоянно. Но это еще не все.

Аргументы и типы событий

Наш обработчик событий помимо вызова при срабатывании события совершает и другие действия — обеспечивает доступ к основному объекту события в виде своего аргумента. Чтобы с легкостью обращаться к объекту события, нужно изменить сигнатуру обработчика событий этого аргумента.

Вот пример, в котором мы указываем имя event, чтобы сослаться на наш аргумент события:

function myEventHandler(event) {

// материал для обработки событий для поддержки

}

Обработчик событий — это по-прежнему простая скучная функция, которая просто получает один аргумент — аргумент события. Можно использовать любой доступный идентификатор для этого аргумента, но я склоняюсь к использованию event или просто e, потому что именно так делают все крутые ребята. Тем не менее технически будет верен и следующий идентификатор:

function myEventHandler(isNyanCatReal) {

// материал для обработки событий

}

Нюанс в том, что аргумент события указывает на объект события и этот объект передается как часть срабатывания события. Есть причина, по которой мы уделяем внимание такому, казалось бы, типичному и скучному явлению. Этот объект события содержит свойства, которые связаны со сработавшим событием. Событие, запущенное кликом мыши, будет иметь отличные свойства в сравнении с событием, запущенным нажатием клавиши клавиатуры, загрузкой страницы, анимацией и многим другим. Большинство событий будут иметь свое собственное уникальное поведение, на которое мы будем опираться, а объект события — это наше окно в эту их уникальность.

Несмотря на разнообразие событий и итоговых объектов событий, которые мы можем получить, у них есть некоторые общие свойства. Эта общая часть определяется тем, что все объекты событий происходят от основного типа Event (технически подразумевается Interface). Ниже перечислены некоторые из известных свойств типа Event, которые мы будем использовать:

1. currentTarget

2. target

3. preventDefault

4. stopPropagation

5. type

Чтобы как следует понять назначение этих свойств, нужно углубиться в понимание самих событий. Мы пока еще не достигли нужной глубины, поэтому просто знайте, что они существуют. В ближайших главах мы с ними встретимся.

Удаление слушателя событий

Иногда понадобится удалять слушателя событий из элемента. Делается это с помощью злейшего врага addEventListener функции removeEventListener:

something.removeEventListener(eventName, eventHandler, false);

Как видно из примера, эта функция получает в точности такие же типы аргументов, что и функция addEventListener. Причина тому проста. Когда мы прослушиваем событие в элементе или объекте, JavaScript использует eventName, eventHandler и значение true / false, чтобы опознать слушателя событий. Чтобы удалить этого слушателя событий, нам нужно указать в точности такие же аргументы. Вот пример:

document.addEventListener("click", changeColor, false);

document.removeEventListener("click", changeColor, false);


function changeColor() {

document.body.style.backgroundColor = "#FFC926";

}

Слушатель событий, которого мы добавили в первой строке, полностью нейтрализован вызовом removeEventListener в выделенной второй строке. Если вызов removeEventListener использует аргументы, отличные от указанных в соответствующем вызове addEventListener, то его усилия будут проигнорированы и прослушивание продолжится.

КОРОТКО О ГЛАВНОМ

Что ж, это все, что нужно для ознакомления с событиями. Помните: у вас есть функция addEventListener, которая позволяет зарегистрировать функцию обработчика событий. Эта функция обработчика событий будет вызываться при запуске события, которое примет прослушиватель событий. Хотя мы коснулись некоторых других тем, они станут более понятными, если рассмотреть их в контексте более сложных примеров, связанных с событиями.

Глава 32. Всплытие и погружение событий

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

Сейчас я превращусь в Шерлока Холмса и раскрою вам тайну, что же именно происходит при срабатывании события. Вы узнаете о двух фазах, в которых пребывают события, поймете, почему это важно знать, а также познакомитесь с некоторыми трюками, которые помогут лучше контролировать события.

Событие опускается. Событие поднимается

Для наглядности оформим все в виде простого примера:


Events!




Вроде бы ничего особенного здесь не происходит. 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!. Это, вероятно, их лучший эпизод, в котором Майкл Ричардс и Джерри Сайнфелд попивают кофе и беседуют о событиях, фазе всплытия и прочих, на мой взгляд, важных вещах.

Глава 33. События мыши

Один из наиболее распространенных способов взаимодействия с компьютером — это использование мыши (рис. 33.1).

Рис. 33.1. Кошки их тоже наверняка любят

Перемещая это волшебное устройство и нажимая на его кнопки, можно совершать множество полезных действий. При этом использование мыши простым пользователем — это одно. Для разработчика же согласование работы кода с действиями мыши — уже совсем другое. Здесь мы и начнем очередную главу.

Знакомьтесь с событиями мыши

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

• click

• dblclick

• mouseover

• mouseout

• mouseenter

• mouseleave

• mousedown

• mouseup

• mousemove

• contextmenu

• mousewheel и DOMMouseScroll

Названия этих событий могут уже дать некоторое представление об их назначении, но мы, тем не менее, рассмотрим каждое из них подробнее. Хочу сразу предупредить вас, что некоторые из событий ужасно скучны.

Одинарный или двойной клик

Начнем с наиболее часто используемого события мыши — клика. Это событие срабатывает, когда вы щелкаете по элементу. Иначе можно сказать, что событие click срабатывает, когда вы используете мышь для нажатия на элемент, а затем отпускаете нажатие, сохраняя курсор на этом элементе.

Ниже — абсолютно бесполезная визуализация сказанного:

Вы уже несколько раз видели код для работы с событием клика, но его никогда не бывает достаточно, поэтому вот вам еще один пример:

let button = document.querySelector("#myButton");

button.addEventListener("click", doSomething, false);


function doSomething(e) {

console.log("Mouse clicked on something!");

}

Прослушивание события click аналогично практически любому другому событию, поэтому не стану утомлять вас подробностями нашей старой знакомой функции addEventListener. Вместо этого утомлю вас подробностями, связанными с событием dblclick.

dblclick срабатывает, когда вы быстро щелкаете мышью дважды. Код для использования этого события будет следующим:

let button = document.querySelector("#myButton");

button.addEventListener("dblclick", doSomething, false);


function doSomething(e) {

console.log("Mouse clicked on something…twice!");

}

Время между кликами, определяющее срабатывание события dblclick, заложено в ОС, где происходит выполнение кода. Оно не зависит от браузера и не определяется (не считывается) пользователем с помощью JavaScript.

НЕ ПЕРЕГРУЖАЙТЕ

Если вдруг случится прослушивать и событие click, и событие dblclick для элемента, обработчики событий будут вызваны три раза при двойном щелчке. Вы получите два события щелчка, которые будут соответствовать каждому щелчку. После второго щелчка вы также получите событие dblclick.

Наведение и отведение курсора

Классические сценарии наведения и отведения курсора обрабатываются логично названными событиями mouseover и mouseout соответственно:

Вот фрагмент кода с применением этих событий:

let button = document.querySelector("#myButton");

button.addEventListener("mouseover", hovered, false);

button.addEventListener("mouseout", hoveredOut, false);


function hovered(e) {

console.log("Hovered!");

}


function hoveredOut(e) {

console.log("Hovered Away!");

}

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

Что насчет двух других похожих событий?

Мы рассмотрели только два события (mouseover и mouseout), которые срабатывают при наведении курсора на что-либо и его отведении. На деле же оказывается, что есть еще два события, которые делают то же самое, — mouseenter и mouseleave. Уникальность этих событий обусловливается одной важной деталью, а именно тем, что они не всплывают.

Это важно, только если интересующий вас элемент имеет потомков. Все эти четыре события ведут себя одинаково, если в процессе не присутствуют потомки. Если же таковые присутствуют, тогда:

• mouseover и mouseout будут срабатывать каждый раз, когда вы наводите курсор на потомка. Это значит, что можно увидеть срабатывание многих ненужных событий, несмотря на то что курсор двигается внутри одной области.

• mouseenter и mouseleave будут срабатывать только единожды. При этом не важно, через сколько потомков вы переместите курсор мыши.

В 90 % случаев вам вполне подойдут mouseover и mouseout. В остальных случаях, которые зачастую связаны с более сложными сценариями UI, вас порадует, что существуют такие события, как mouseenter и mouseleave.

События mousedown и mouseup

Два события, которые практически являются субкомпонентами события click, — это mousedown и mouseup. Следующая диаграмма поясняет почему:

Когда вы нажимаете на кнопку мыши, срабатывает событие mousedown. Когда вы отпускаете нажатие, срабатывает событие mouseup. Если нажатие и отпускание произошло на одном и том же элементе, тогда также сработает событие click.

Все это показано в следующем фрагменте кода:

let button = document.querySelector("#myButton");

button.addEventListener("mousedown", mousePressed, false);

button.addEventListener("mouseup", mouseReleased, false);

button.addEventListener("click", mouseClicked, false);


function mousePressed(e) {

console.log("Mouse is down!");

}


function mouseReleased(e) {

console.log("Mouse is up!");

}


function mouseClicked(e) {

console.log("Mouse is clicked!");

}

Справедливый вопрос: «Зачем заморачиваться этими двумя событиями?» Кажется, что событие click идеально подходит для большинства случаев, в которых может понадобиться использовать mousedown и mouseup. Вы правы, можно не замарачиваться. При этом будет полезно пояснить, что события mousedown и mouseup просто дают больше контроля, когда он нужен. Некоторые взаимодействия (вроде перетаскиваний или отпадных приемов в видеоиграх, когда вы задерживаете нажатие, чтобы зарядить смертоносный удар молнии) подразумевают совершение действий, когда сработало только событие mousedown, но не mouseup.

Событие услышано снова… и снова… и снова!

Одним из самых болтливых событий, с которыми вам предстоит работать, является событие mousemove. Оно срабатывает огромное количество раз во время движения курсора по элементу, в котором вы прослушиваете событие mousemove:

Далее приведен пример использования mousemove в коде:

let button = document.querySelector("#myButton");

button.addEventListener("mousemove", mouseIsMoving, false);


function mouseIsMoving(e) {

console.log("Mouse is on the run!");

}

Ваш браузер контролирует частоту, с которой срабатывает событие mousemove, при этом оно срабатывает, когда курсор мыши смещается даже всего на один пиксель. Это событие хорошо для многих интерактивных сценариев, в которых, к примеру, важно отслеживать текущую позицию курсора.

Контекстное меню

Последним связанным с мышью событием, которое мы рассмотрим, является contextmenu. Как вам наверняка хорошо известно, когда вы по обыкновению кликаете правой кнопкой мыши в различных приложениях, появляется меню:

Оно называется контекстное меню. Как раз перед появлением этого меню срабатывает событие contextmenu.

Честно говоря, есть всего одна весомая причина для прослушивания этого события. Она связана с предотвращением появления этого меню при правом клике, использовании связанной с ним клавиши клавиатуры или просто горячей клавиши.

Вот пример того, как вы можете предотвратить встроенное поведение, при котором появляется контекстное меню:

document.addEventListener("contextmenu", hideMenu, false);


function hideMenu(e) {

e. preventDefault();

}

Метод preventDefault в любом типе Event предотвращает любое его встроенное действие. Так как событие contextmenu срабатывает до появления меню, вызов preventDefault гарантирует, что оно показано не будет. Да, я уже второй раз упоминаю это свойство, но вы же знаете, что мне платят за количество символов (ха-ха).

Учитывая все сказанное, я могу придумать множество альтернативных способов для предотвращения появления контекстного меню без помощи событий, но пока что дела обстоят именно так.

Свойства MouseEvent

Давайте перейдем к конкретике. Все события мыши, которые мы видели до сих пор, основаны на MouseEvent. Обычно эту разновидность фактоида[4] вы храните исключительно для торжественных случаев и игнорируете. Тем не менее в данном случае эта деталь для нас важна, так как MouseEvent несет в себе набор свойств, упрощающих работу с мышью. Давайте на них посмотрим.

Глобальная позиция мыши

Свойства screenX и screenY возвращают расстояние, на котором находится курсор мыши от левого верхнего угла основного монитора:

Вот очень простой пример использования screenX и screenY:

document.addEventListener("mousemove", mouseMoving, false);


function mouseMoving(e) {

console.log(e.screenX + " " + e.screenY);

}

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

Позиция курсора мыши в браузере

Свойства clientX и clientY возвращают позиции x и y курсора относительно левого верхнего угла браузера (технически его области просмотра):

Код в данном случае достаточно прост:

let button = document.querySelector("#myButton");

document.addEventListener("mousemove", mouseMoving, false);


function mouseMoving(e) {

console.log(e.clientX + " " + e.clientY);

}

Вы просто вызываете свойства clientX и clientY аргумента события, переданного в обработчик события, чтобы получить их значения.

Определение нажатой кнопки

Мыши зачастую оборудованы несколькими кнопками или предоставляют возможность их имитировать. Наиболее распространенная конфигурация состоит из левой, правой и средней (обычно это нажатие на колесико мыши) кнопок. Для определения, какая из кнопок была нажата, существует свойство button. Это свойство возвращает 0 при нажатии левой кнопки, 1 — при нажатии средней и 2 — при нажатии правой:

Код для использования этого свойства выглядит вполне ожидаемо:

document.addEventListener("mousedown", buttonPress, false);


function buttonPress(e) {

if (e.button == 0) {

console.log("Left mouse button pressed!");

} else if (e.button == 1) {

console.log("Middle mouse button pressed!");

} else if (e.button == 2) {

console.log("Right mouse button pressed!");

} else {

console.log("Things be crazy up in here!!!");

}

}

В дополнение к свойству button существует свойство buttons, а также другие, которые делают нечто похожее для помощи в определении нажатой кнопки. Я не буду много говорить об этих свойствах, просто имейте в виду, что они существуют (вы всегда можете погуглить их).

Работа с колесиком мыши

Колесико мыши отличается от всего, что мы рассмотрели до этого момента. Очевидная разница в том, что здесь мы уже имеем дело с колесиком, а не кнопкой. Менее же очевидное, но при этом более важное отличие в том, что в данном случае вы используете уже два события для работы. Первое — это mousewheel, используемое в Internet Explorer и Chrome, а второе — это DOMMouseScroll, используемое в Firefox.

Прослушивание этих событий производится обычным образом:

document.addEventListener("mousewheel", mouseWheeling, false);

document.addEventListener("DOMMouseScroll", mouseWheeling, false);

А вот после уже есть нюансы. События mousewheel и DOMMouseScroll будут срабатывать в момент прокручивания колесика в любом направлении. Но для любой практической цели будет важно, в каком направлении происходит прокрутка. Чтобы получить эту информацию, пороемся в обработчике событий и найдем аргумент события.

Аргументы события для события mousewheel содержат свойство под названием wheelDelta. В случае же с DOMMouseScroll в аргументе события присутствует свойство detail. Оба этих свойства похожи в том, что их значения изменяются на положительные или отрицательные в зависимости от направления прокрутки колесика. Здесь стоит отметить, что они не согласованы в трактовке положительного и отрицательного значения. Свойство wheelDelta, связанное с событием mousewheel, становится положительным при прокрутке вверх и отрицательным при прокрутке вниз. В точности наоборот происходит в случае со свойством DOMMouseScroll. При прокрутке вверх оно дает отрицательное значение, а при прокрутке вниз — положительное.

Из следующего примера видна обработка этой несогласованности свойств wheelDelta и detail, которая весьма проста:

function mouseWheeling(e) {

let scrollDirection;

let wheelData = e.wheelDelta;


if (wheelData) {

scrollDirection = wheelData;

} else {

scrollDirection = -1 * e.detail;

}


if (scrollDirection > 0) {

console.log("Scrolling up! " + scrollDirection);

} else {

console.log("Scrolling down! " + scrollDirection);

}

}

Переменная scrollDirection хранит значение, содержащееся в свойстве wheelData или detail. Вы можете определить особое поведение в зависимости от того, является значение положительным или отрицательным.

КОРОТКО О ГЛАВНОМ

Если вы умеете работать с одним событием, то, значит, понимаете основу работы со всеми остальными. Главное — знать, какое событие соответствует нужным вам действиям. Знакомство с событиями мыши — это хорошее начало для освоения работы с событиями в принципе, так как ничего сложного в этом нет. Они не отличаются беспорядочностью, и то, что вы о них узнаете, вы будете использовать практически во всех своих приложениях.

Дополнительные ресурсы и примеры, которые могут вас заинтересовать:

• Перемещение элемента в место клика: http://bit.ly/kirupaElementClickPosition

• Вы используете сенсорное устройство? http://bit.ly/kirupaTouchEnabled

Если у вас есть какие-либо вопросы, уделите им время и обратитесь на форум https://forum.kirupa.com.

Глава 34. События клавиатуры

Работая в приложениях, очень много внимания мы уделяем клавиатуре. Если вдруг вам интересно, как выглядит это устройство, то на рис. 34.1 показан один из экземпляров, вероятно, столетней давности.

Рис. 34.1. Так выглядит музейный экспонат клавиатуры

Как бы то ни было, наши компьютеры (а именно выполняемые на них приложения) умеют взаимодействовать с этими усыпанными клавишами пластиковыми досками. Вас же этот процесс интересует редко. Однако в нашем случае в зависимости от выполняемой задачи думать об этом взаимодействии придется регулярно. Если быть точнее, то придется не только думать о нем, но и научиться его налаживать. Поэтому лучше отмените все ближайшие планы, так как текущая глава будет насыщенной.

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

Поехали!

Знакомьтесь с событиями клавиатуры

Для работы с клавиатурой в HTML-документе вам потребуется познакомиться с тремя событиями, а именно:

• keydown;

• keypress;

• keyup.

Глядя на названия этих событий, вы наверняка уже догадались, за что они отвечают. keydown срабатывает при нажатии клавиши, а keyup — при ее отпускании. Оба этих события работают для любой клавиши, с которой вы взаимодействуете.

Событие же keypress — это отдельный случай. На первый взгляд может показаться, что оно срабатывает при нажатии клавиши. Но, несмотря на название, срабатывает оно, только когда вы нажимаете на клавишу, которая отображает знак (букву, цифру и т. п.). Это может показаться не совсем понятным, но своеобразный смысл здесь все-таки присутствует.

Если вы нажмете и отпустите клавишу знака вроде буквы y, то увидите, что по порядку сработали события keydown, keypress и keyup. В данном случае keydown и keyup сработали потому, что клавиша y для них — просто клавиша. Событие же keypress сработало, так как клавиша y — это клавиша знака. Если вы нажмете и отпустите клавишу, которая на экране ничего не отображает (например, пробел, стрелка или функциональные клавиши), то увидите, что сработали только события keydown и keyup.

Это неявное отличие, но оно очень важно, чтобы нажатия клавиш были услышаны приложением.

Что сказал?

Странно, что событие под названием keypress не срабатывает при нажатии любой клавиши. Может быть, это событие следует назвать как-то иначе, например characterkeypress, но это, скорее всего, проблема МУ (все равно, что мнение коровы и ее мнение никого не волнует). (Подробнее о проблеме МУ здесь: http://bit.ly/kirupaMoo)

Использование событий

Прослушивание событий keydown, keyup и keypress аналогично любым другим событиям, которые мы прослушиваем и на которые реагируем. Вы вызываете addEventListener для элемента, который будет работать с этим событием, указываете событие, которое нужно прослушать, указываете функцию обработчика событий, которая вызывается, когда событие услышано, а также указываете значение trueилиfalse, определяя, должно ли оно прослушиваться в фазе всплытия.

Вот пример прослушивания трех событий клавиатуры в объекте window:

window.addEventListener("keydown", dealWithKeyboard, false);

window.addEventListener("keypress", dealWithKeyboard, false);

window.addEventListener("keyup", dealWithKeyboard, false);


function dealWithKeyboard(e) {

// вызывается, когда услышано любое из событий клавиатуры

}

Как только любое из этих событий будет услышано, последует вызов обработчика событий dealWithKeyboard. На деле же при нажатии клавиши знака этот обработчик будет вызван трижды. Все это вполне понятно, поэтому давайте увеличим уровень сложности и в последующих разделах уже выйдем за рамки основ.

Свойства события Keyboard

Когда происходит вызов обработчика событий, передается аргумент события Keyboard. Давайте вернемся к обработчику событий dealWithKeaboard из предыдущего раздела. В нем событие клавиатуры представлено передаваемым аргументом e:

function dealWithKeyboard(e) {

// вызывается, когда услышано любое событие клавиатуры

}

Этот аргумент содержит несколько свойств:

• KeyCode. Каждая клавиша клавиатуры имеет связанное с ней число. Это число возвращается свойством только для чтения.

• CharCode. Это свойство существует только в аргументах события, возвращенных событием keypress, и содержит код ASCII для любой нажатой клавиши знака.

• ctrlKey, altKey, shiftKey. Эти три свойства возвращают true, если нажата клавиша Ctrl, Alt или Shift.

• MetaKey. Это свойство похоже на ctrlKey, altKey и shiftKey тем, что возвращает true, если нажата клавиша Meta на клавиатурах Windows или клавиша Command на клавиатурах Apple.

Событие Keyboard содержит и другие свойства, но приведенные выше являются наиболее интересными. Используя их, вы можете проверять, какая была нажата клавиша, и реагировать соответственно. В двух последующих разделах вы увидите примеры.

ВНИМАНИЕ

Свойства charCode и keyCode считаются специалистами по веб-стандартам W3C устаревшими. Их альтернативой может стать мало поддерживаемое свойство code. Просто имейте это в виду и будьте готовы к обновлению кода, как только на трон взойдет преемник этих свойств.

Примеры

Теперь, когда скучные основы работы с событиями клавиатуры позади, пора рассмотреть некоторые примеры, которые прояснят (а может, и запутают) все пройденное.

Проверка нажатия конкретной клавиши

Следующий пример показывает, как использовать свойство keyCode для проверки нажатия конкретной клавиши:

window.addEventListener("keydown", checkKeyPressed, false);


function checkKeyPressed(e) {

if (e.keyCode == 65) {

console.log("The 'a' key is pressed.");

}

}

Здесь я проверяю клавишу a. Внутренне эта клавиша отображается значением 65 свойства keyCode. В случае если вы так и не зазубрили их в школе, можете обратиться к подручному списку кодов для всех клавиш и знаков по следующей ссылке: http://bit.ly/kirupaKeyCode.

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

Прошу отметить следующее. Значения charCode и keyСode для конкретной клавиши не будут одинаковы. Помимо этого, charCode возвращается, только если обработчик событий сработал в ответ на keypress. В нашем примере событие keydown не содержало бы ничего полезного для свойства charCode.

Если вы захотите проверить charCode и использовать событие keypress, то предыдущий пример будет выглядеть так:

window.addEventListener("keypress", checkKeyPressed, false);


function checkKeyPressed(e) {

if (e.charCode == 97) {

console.log("The 'a' key is pressed.");

}

}

charCode для клавиши a — это 97. Опять же напомню, подробности можно взять из таблицы, приведенной по ссылке выше.

Совершение действий при нажатии клавиш стрелок

Чаще всего мы встречаемся с этим в играх, где нажатие стрелок приводит к выполнению действий. В следующем листинге показана реализация:

window.addEventListener("keydown", moveSomething, false);


function moveSomething(e) {

switch (e.keyCode) {

case 37:

// нажатие влево

break;

case 38:

// нажатие вверх

break;

case 39:

// нажатие вправо

break;

case 40:

// нажатие вниз

break;

}

}

Здесь все вполне понятно. Помимо прочего, это актуальный пример использования инструкции switch, которую вы изучили еще в далекой главе 4.

Определение нажатия нескольких клавиш

А вот теперь эпичная часть! Самое интересное связано с определением нажатия нескольких клавиш и соответственного реагирования. Далее показано, как это можно сделать:

window.addEventListener("keydown", keysPressed, false);

window.addEventListener("keyup", keysReleased, false);


let keys = [];


function keysPressed(e) {

// сохраняет запись о каждой нажатой клавише

keys[e.keyCode] = true;


// Ctrl + Shift + 5

if (keys[17] && keys[16] && keys[53]) {

// делает что-нибудь

}


// Ctrl + f

if (keys[17] && keys[70]) {

// делает что-нибудь


// предотвращает встроенное поведение браузера

e. preventDefault();

}

}


function keysReleased(e) {

// отмечает отпущенные клавиши

keys[e.keyCode] = false;

}

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

Во-первых, имеется массив клавиш, хранящий каждую клавишу, которую вы нажимаете:

let keys = [];

По мере нажатия клавиш происходит вызов обработчика событий keysPressed:

function keysPressed(e) {

// хранит запись о каждой нажатой клавише

keys[e.keyCode] = true;


// Ctrl + Shift + 5

if (keys[17] && keys[16] && keys[53]) {

// делает что-нибудь

}

// Ctrl + f

if (keys[17] && keys[70]) {

// делает что-нибудь


// предотвращает встроенное поведение браузера

e. preventDefault();

}

}

Когда клавиша отпускается, происходит вызов обработчика событий keysRealesed:

function keysReleased(e) {

// помечает отпущенные клавиши

keys[e.keyCode] = false;

}

Обратите внимание, как эти два обработчика событий взаимодействуют. При нажатии клавиш для них создается запись в массиве keys со значением true. При отпускании же клавиш их значения меняются на false. Существование в массиве нажимаемых вами клавиш поверхностно, важны лишь хранимые ими значения.

До тех пор пока ничего вроде всплывающего окна уведомления не мешает правильному вызову ваших обработчиков событий, с позиции массива keys будет возникать взаимно однозначное соответствие между нажатыми и отпущенными клавишами. Учитывая это, проверка определения комбинации нажатых клавиш производится в обработчике событий keysPressed. Выделенные в следующем коде строки показывают, как это работает:

function keysPressed(e) {

// хранит запись о каждой нажатой клавише

keys[e.keyCode] = true;


// Ctrl + Shift + 5

if (keys[17] && keys[16] && keys[53]) {

// делает что-то

}


// Ctrl + f

if (keys[17] && keys[70]) {

// делает что-то


// предотвращает стандартное поведение браузера

e. preventDefault();

}

}

Важно учитывать одну деталь: некоторые комбинации клавиш приводят к реагированию браузера. Чтобы избежать выполнения браузером ненужных вам действий, используйте метод preventDefault, как это выделено, при проверке использования Ctrl + F:

function keysPressed(e) {

// хранит запись о каждой нажатой клавише

keys[e.keyCode] = true;


// Ctrl + Shift + 5

if (keys[17] && keys[16] && keys[53]) {

// делает что-то

}


// Ctrl + f

if (keys[17] && keys[70]) {

// делает что-то


// предотвращает стандартное поведение браузера

e. preventDefault();

}

}

Метод preventDefault предотвращает стандартную реакцию браузера на событие. В данном случае он не дает браузеру показать диалоговое окно поиска. Различные комбинации клавиш будут запускать различные реакции браузера, поэтому держите этот метод на вооружении, чтобы исключить их.

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

КОРОТКО О ГЛАВНОМ

Клавиатура выступает важным элементом взаимодействия пользователей с компьютерами и аналогичными им устройствами. Несмотря на это, не всегда приходится иметь с ней дело напрямую. Ваш браузер, различные относящиеся к тексту элементы управления и аналогичные им компоненты уже справляются с этим по умолчанию. Тем не менее есть определенные приложения, где может потребоваться непосредственно поработать с клавиатурой. Этому и была посвящена текущая глава.

Она началась максимально скучно с объяснения принципа работы событий Keyboard и их аргументов событий. По мере продвижения все становилось интереснее: вы увидели некоторые примеры кода, где были показаны действия с клавиатурой. Если у вас есть какие-либо вопросы по этой или другой теме, не стесняйтесь обращаться с ними на форум https://forum.kirupa.com.

Глава 35. События загрузки страницы и прочее

Важной частью работы с JavaScript является обеспечение выполнения кода в нужное время. И часто все не так просто, чтобы лишь поместить код вниз страницы и ждать, что все начнет работать, как только страница загрузится. Здесь мы еще раз вернемся к некоторым моментам из главы 10. Во многих случаях вам может понадобиться добавить дополнительный код, чтобы гарантировать, что код не будет запущен, пока страница не будет к этому готова. А иногда может даже понадобиться поместить код именно в начало страницы.

Есть множество факторов, влияющих на выбор «подходящего времени» для запуска кода. В этой главе мы рассмотрим эти факторы и сформируем из пройденного материала небольшое полезное руководство.

Поехали!

Что происходит в процессе загрузки страницы

Начнем с самого начала. Вы щелкаете по ссылке либо нажимаете Enter после набора URL, и если сошлись звезды, загружается страница. Все кажется очень простым и занимает мало времени:

За этот короткий отрезок времени происходит множество связанных с ним интересных процессов, о которых следует знать подробнее. Одним из примеров таких процессов является выполнение заданного для страницы кода. Точный момент выполнения этого кода зависит от сочетания следующих компонентов, которые оживают в определенный момент загрузки страницы:

• событие DOMContentLoaded;

• событие load;

• атрибут async элементов сценария;

• атрибут defer элементов сценария;

• место расположения сценария в DOM.

Не переживайте, если перечисленное вам не знакомо. Очень скоро вы узнаете о назначении всех этих штук. Но сначала рассмотрим три стадии загрузки страницы.

Стадия первая

Первая стадия охватывает момент, когда браузер вот-вот начнет загрузку страницы.

Во время этой стадии не происходит ничего интересного. Запрос на загрузку страницы уже был сделан, но еще ничего не загружено.

Стадия вторая

Здесь уже более насыщенный процесс, во время которого происходят загрузка и обработка сырой разметки, а также DOM-страницы.

Здесь стоит отметить, что внешние ресурсы вроде изображений и связанных с ними таблиц стилей еще не считаны. Вы видите только сырое содержимое, определенное разметкой страницы или документа.

Стадия третья

Во время заключительной стадии страница уже полностью загружена со всеми изображениями, таблицами стилей, сценариями и прочими внешними ресурсами, преобразованными в то, что вы видите:

На этой стадии индикаторы загрузки браузера прекращают анимацию, и именно здесь вы почти всегда оказываетесь при взаимодействии с HTML-документом. Учитывая все сказанное, иногда страница может оказаться в промежуточном состоянии, когда 99 % контента загрузилось, но какой-то случайный элемент застревает в загрузке навечно. Если вы посещали один из вирусных, или информационных, или фид-сайтов, то прекрасно поймете, о чем я.

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

DOMContentLoaded и load Events

Есть два события, представляющих два основных ориентира в процессе загрузки страницы: DOMContentLoaded и load. DOMContentLoad срабатывает в конце стадии 2, когда DOM страницы полностью обработан. Событие load срабатывает в конце стадии 3, как только страница полностью завершает загрузку. Вы можете использовать эти события для выбора времени выполнения кода.

Ниже приведен фрагмент кода с использованием этих двух событий:

document.addEventListener("DOMContentLoaded", theDomHasLoaded,

false);

window.addEventListener("load", pageFullyLoaded, false);


function theDomHasLoaded(e) {

// делает что-нибудь

}


function pageFullyLoaded(e) {

// снова делает что-нибудь

}

Вы используете эти события так же, как и любые другие, но при этом важно учесть, что вам надо прослушивать DOMContentLoaded из элемента document, а load — из элемента window.

Теперь, когда со всеми скучными техническими деталями покончено, подумаем, почему эти события важны? Очень просто. Если у вас есть код, опирающийся на работу с DOM вроде всего того, что использует querySelector или querySelectorAll, то вам нужно обеспечить, чтобы этот код запускался только после полной загрузки DOM. Если вы попробуете обратиться к DOM до этого момента, то либо получите неполные результаты, либо не получите их вообще.

Вот прекрасный радикальный пример от Кайла Мюррея:



[Вставьте здесь полную копию /Войны и мира/]


Верный способ избежать ситуации, в которой код запускается до момента готовности DOM, — это прослушать событие DOMContentLoaded и установить запуск кода, опирающегося на DOM только тогда, когда это событие будет услышано:

document.addEventListener("DOMContentLoaded", theDomHasLoaded, false);


function theDomHasLoaded(e) {

let headings = document.querySelectorAll("h2");


// делает что-нибудь с изображениями

}

Для случаев, когда нужно, чтобы код запускался только после полной загрузки страницы, используйте событие load. За все годы использования JavaScript мне не так часто приходилось использовать это событие на уровне документа, за исключением проверки итоговых размеров загруженного изображения или создания простых индикаторов прогресса.

Сценарии и их расположение в DOM

В главе 8 мы рассмотрели различные способы для определения положения сценариев внутри документа. Вы видели, что положение элементов в DOM влияет на момент запуска. В этом разделе подтвердим эту простую истину и немного углубимся.

Вспомним, что простой элемент сценария может быть встроенным кодом в какой-то части документа:

Он также может быть чем-то, что ссылается на некий код во внешнем файле:

А теперь важная деталь относительно этих элементов. Ваш браузер считывает DOM последовательно сверху вниз. Любые элементы сценария, встречающиеся на его пути, будут считаны в том порядке, в каком они расположены в DOM.

Ниже приведен очень простой пример со множеством элементов сценария:


Example


Не важно, содержит ли сценарий встроенный код или ссылается на внешний источник, — все сценарии рассматриваются одинаково и запускаются в том порядке, в котором расположены в документе. В верхнем примере порядок выполнения сценариев будет следующим: inline 1, external 1, inline 2, external 2 и в конце inline 3.

А вот еще одна, но уже очень важная деталь, которую необходимо учитывать. Так как DOM считывается сверху вниз, ваш элемент сценария имеет доступ ко всем элементам DOM, которые уже были считаны. И наоборот, ваш сценарий не имеет доступа к еще не считанным элементам DOM. Как вам такое?

Предположим, есть элементы сценария, расположенные внизу страницы чуть выше закрывающего тело элемента:


Example


Quisque faucibus, quam sollicitudin pulvinar dignissim, nunc

velit sodales leo, vel vehicula odio lectus vitae mauris. Sed

sed magna augue. Vestibulum tristique cursus orci, accumsan

posuere nunc congue sed. Ut pretium sit amet eros non consectetur.

Quisque tincidunt eleifend justo, quis molestie tellus venenatis

non. Vivamus interdum urna ut augue rhoncus, eu scelerisque

orci dignissim. In commodo purus id purus tempus commodo.




Когда выполняется something.js, он может обратиться ко всем элементам DOM, находящимся над ним, вроде h1, p и button. Если ваш элемент сценария расположен в верхней части документа, он не будет знать о других элементах DOM, расположенных ниже него:



Example


Quisque faucibus, quam sollicitudin pulvinar dignissim, nunc

velit sodales leo, vel vehicula odio lectus vitae mauris. Sed

sed magna augue. Vestibulum tristique cursus orci, accumsan

posuere nunc congue sed. Ut pretium sit amet eros nonconsectetur.

Quisque tincidunt eleifend justo, quis molestie tellus venenatis

non. Vivamus interdum urna ut augue rhoncus, eu scelerisque

orci dignissim. In commodo purus id purus tempus commodo.




При размещении элемента сценария внизу страницы, как было показано ранее, его конечное поведение будет таким же, будто есть код, явно слушающий событие DOMContentLoaded. Если вы сможете сделать так, что сценарии появятся ближе к концу документа, после всех элементов DOM, то полностью избежите использования подхода DOMContentLoaded, описанного в предыдущем разделе. Итак, если вам действительно нужно расположить элементы сценария в верхней части DOM, обеспечьте, чтобы весь код, опирающийся на DOM, выполнялся после срабатывания события DOMContentLoaded.

В этом вся суть. Я большой поклонник размещения элементов сценария в нижней части DOM. Есть и еще одна причина кроме упрощенного доступа к DOM, почему я рекомендую располагать сценарии внизу страницы. Когда элемент сценария считывается, браузер приостанавливает выполнение всего остального на странице на время выполнения его кода. Если речь идет о длительно выполняемом сценарии или внешнем сценарии, требующем время на загрузку, HTML-страница будет попросту заморожена. Если же на этот момент ваша DOM будет считана лишь частично, то страница помимо остановки еще и будет выглядеть незавершенной. А это вряд ли кому-то понравится.

Элементы сценария async и defer

В предыдущем разделе я объяснил, как расположение элементов сценария в DOM определяет время их запуска. Все это относится только к тем элементам, которые я называю простыми. Чтобы стать частью непростого мира, элементы сценария, указывающие на внешние сценарии, могут содержать атрибуты defer и async:

Эти атрибуты изменяют время запуска сценария вне зависимости от того, где в DOM они фактически расположены. Посмотрим, как они это делают.

async

Атрибут async позволяет сценарию выполняться асинхронно:

Если вспомнить предыдущий раздел, то на время считывания элемента браузер может заблокироваться и стать недееспособным. Установив атрибут async в элементе сценария, вы полностью избегаете этой проблемы. Сценарий выполнит все, что должен, и при этом ничто не помешает браузеру заниматься своими делами.

Такая беспечность в выполнении кода довольно удивительна, но важно понимать, что сценарии, отмеченные как async, не всегда будут запускаться по порядку. Может случиться, что несколько таких сценариев будут запущены в последовательности, отличной от указанной в разметке. Точно известно лишь то, что сценарии с async начнут выполнение в некоей загадочной точке до срабатывания события load.

defer

Атрибут defer несколько отличен от async:

Сценарии, помеченные defer, запускаются в том порядке, в каком были определены, но выполняются только в самом конце, за несколько мгновений до срабатывания события DOMContentLoaded. Взгляните на следующий пример:


Example


Задумайтесь на секунду и расскажите находящемуся рядом человеку (или животному), в каком порядке эти сценарии будут запущены. При этом можете не пояснять контекст, ведь если они вас любят, то обязательно поймут.

Запустятся они в такой последовательности: inline 1, external 2, inline 2, inline 3, external 3, а затем external 1. Сценарии external 3 и external 1 помечены как defer, именно поэтому они оказываются в конце, несмотря на свое положение в разметке.

КОРОТКО О ГЛАВНОМ

В последних разделах мы рассмотрели факторы, влияющие на время запуска кода. Схема ниже объединяет весь этот материал:

Теперь перейдем к актуальному для вас вопросу. Какое время будет наилучшим для выполнения вашего кода JavaScript? Важно добиться следующего:

1. Ссылки на сценарии располагайте ниже DOM, сразу над закрывающим body элементом.

2. Если вы не создаете библиотеку для других пользователей, не усложняйте код прослушиванием событий DOMContentLoaded или load. Прочтите предыдущий пункт.

3. Помечайте сценарии, ссылающиеся на внешние файлы, атрибутом defer.

4. Если у вас есть код, не зависящий от загрузки DOM и выполняемый как часть разветвления других сценариев в документе, его можно поместить вверх страницы, снабдив атрибутом async.

Вот и все. Думаю, что этих четырех рекомендаций хватит, чтобы в 90 % случаев обеспечить своевременный запуск кода. Для более продвинутых сценариев следует рассмотреть сторонние библиотеки вроде require.js, которые дают больший контроль над временем запуска кода. Если у вас возникнут какие-либо сложности с загрузкой, обращайтесь на https://forum.kirupa.com.

Дополнительные ресурсы и примеры:

• Загрузка модулей с помощью RequireJS: http://bit.ly/kirupaRequireJS

• Предварительная загрузка изображений: http://bit.ly/kirupaPreloadImages

Глава 36. Обработка событий для нескольких элементов

Базово слушатель событий работает с событиями, запускаемыми одним элементом:

Однако по мере создания более сложных программ отображение «одного обработчика событий для одного элемента» уже не подойдет. Причина в динамическом создании элементов посредством JavaScript. Эти элементы могут запускать события, которые вам может понадобиться прослушать и на которые соответственно среагировать. При этом вам может потребоваться обработка событий как для нескольких элементов, так и для их множества.

Вряд ли вы захотите делать так:

Вам не захочется создавать слушателя событий для каждого элемента, если слушатель событий для всех них одинаков. Причина в том, что это непродуктивно. Каждый из этих элементов несет в себе данные об одном и том же слушателе событий и его свойствах, что может существенно увеличить потребление памяти при добавлении большого количества содержимого. Вам, наоборот, нужен чистый и быстрый способ обработки событий для множества элементов с минимальным количеством повторений и ненужных компонентов. Предпочтительный вариант в этом случае будет выглядеть примерно так:

Все это может звучать несколько нереально, не так ли? Что ж, в этой главе вы узнаете, что это вполне нормально, и научитесь реализовывать подобное, используя всего несколько строк кода JavaScript.

Поехали!

Как все это делается?

Суть в следующем. Вы знаете, как работает простая обработка событий, когда у вас есть один элемент, один слушатель событий и один обработчик событий. Несмотря на то что случай обработки нескольких элементов может казаться иным, воспользовавшись разрывностью событий, разрешить эту проблему достаточно просто.

Представьте, что есть случай, в котором вы хотите прослушивать событие клика в любом из элементов-братьев со значениями idone, two, three, four и five. Давайте дорисуем картину, изобразив DOM следующим образом:

В самом низу расположены элементы, в которых мы хотим прослушивать события. Все они имеют одного родителя в виде элемента с id, равным theDude. Чтобы разрешить проблему обработки этих событий, давайте рассмотрим сначала плохое решение, а затем его удачную альтернативу.

Плохое решение

Так делать не нужно. Мы не хотим создавать слушателя событий для каждой из кнопок:

let oneElement = document.querySelector("#one");

let twoElement = document.querySelector("#two");

let threeElement = document.querySelector("#three");

let fourElement = document.querySelector("#four");

let fiveElement = document.querySelector("#five");


oneElement.addEventListener("click", doSomething, false);

twoElement.addEventListener("click", doSomething, false);

threeElement.addEventListener("click", doSomething, false);

fourElement.addEventListener("click", doSomething, false);

fiveElement.addEventListener("click", doSomething, false);


function doSomething(e) {

let clickedItem = e.target.id;

console.log("Hello " + clickedItem);

}

Очевидная причина так не делать — в нежелании повторять код. Другая причина состоит в том, что для каждого элемента теперь установлено свойство addEventListener. В случае с пятью элементами это не так страшно. Однако все становится куда серьезнее, когда вы работаете с десятками или сотнями элементов, каждый из которых задействует частичку памяти. Еще одна причина в том, что число элементов может варьировать в зависимости от степени адаптивности или динамичности UI. Ваше приложение может добавлять или удалять элементы в зависимости от действий пользователя, что затруднит отслеживание всех индивидуальных слушателей событий, которые могут потребоваться объекту. Наличие же одного всеобщего обработчика событий существенно упрощает весь этот процесс.

Хорошее решение

Хорошее решение вторит схеме, приведенной ранее, где мы используем всего один слушатель событий. Сначала я вас немного запутаю описанием того, как это работает, а затем попытаюсь распутать, приведя пример кода и подробно пояснив все происходящее. Простое и запутывающее решение:

1. Создать один слушатель событий в родительском элементе theDude.

2. Когда произойдет щелчок по любому из элементов one, two, three, four или five, опереться на поведение распространения, присущее событиям, и прерывать их, когда они достигают элемента theDude.

3. (По желанию) Остановить распространение события в родительском элементе, чтобы оно не отвлекало нас своей безудержной беготней по дереву DOM вверх и вниз.

Не знаю, как вы, но я после прочтения этих пунктов точно запутался. Давайте начнем распутываться, обратившись для начала к схеме, более наглядно представляющей описанные действия:

Последним этапом нашего квеста по распутыванию будет код, подробно расписывающий содержимое схемы и все три шага:

let theParent = document.querySelector("#theDude");

theParent.addEventListener("click", doSomething, false);


function doSomething(e) {

if (e.target!= e.currentTarget) {

let clickedItem = e.target.id;

console.log("Hello " + clickedItem);

}

e. stopPropagation();

}

Уделите время и внимательно прочитайте и проанализируйте этот код. Приняв во внимание наши изначальные цели и схему, мы будем слушать событие в родительском элементе theDude:

let theParent = document.querySelector("#theDude");

theParent.addEventListener("click", doSomething, false);

Обработкой этого события занимается один обработчик, которым является функция doSomething:

function doSomething(e) {

if (e.target!= e.currentTarget) {

let clickedItem = e.target.id;

console.log("Hello " + clickedItem);

}

e. stopPropagation();

}

Этот слушатель событий будет вызван каждый раз, когда будет происходить щелчок как в самом элементе theDude, так и в любом из его потомков. Нас же интересуют только события щелчка потомков. Правильным способом игнорировать щелчки по родительскому элементу будет просто избежать выполнения любого кода, если элемент, на котором произошел щелчок (то есть целевое событие), совпадает со слушателем событий (то есть элементом theDude):

function doSomething(e) {

if (e.target!= e.currentTarget) {

let clickedItem = e.target.id;

console.log("Hello " + clickedItem);

}

e. stopPropagation();

}

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

Чтобы остановить распространение события, мы просто вызываем метод stopPropagation:

function doSomething(e) {

if (e.target!= e.currentTarget) {

let clickedItem = e.target.id;

console.log("Hello " + clickedItem);

}

e. stopPropagation();

}

Обратите внимание, что этот код располагается вне инструкции if. Я сделал так, чтобы остановить перемещение события по DOM во всех случаях, как только оно будет услышано.

Объединяя все сказанное

В результате выполнения всего этого кода вы можете щелкнуть по любому потомку theDude и прослушать событие при его распространении вверх:

Поскольку все аргументы событий по-прежнему уникальны для элемента, с которым мы взаимодействуем (то есть источника события), мы также можем распознать и выделить нажатый элемент изнутри обработчика событий, несмотря на то что addEventListener активна только в родителе. Главное в этом подходе то, что он решает обозначенные проблемы. Вы создали всего один обработчик событий, и не важно, сколько в итоге будет потомков у theDude. Этот подход достаточно универсален и способен справиться со всеми ими, не требуя для этого дополнительного изменения кода. Это также значит, что понадобится произвести строгую фильтрацию, если потомками элемента theDude в итоге будут не только кнопки, но и другие важные для вас элементы.

КОРОТКО О ГЛАВНОМ

Некоторое время назад я предложил решение загадки этого многоэлементного троеборья (крутые ребята говорят: MEEC), которое оказалось непродуктивным, но при этом не требовало повторения множества строк кода. До тех пор пока многие разработчики не указали мне на его непродуктивность, я считал его вполне рабочим.

В этом решении использовался цикл for для прикрепления слушателей событий ко всем потомкам родителя (или массива, содержащего HTML-элементы). Вот как выглядел его код:

let theParent = document.querySelector("#theDude");


for (let i = 0; i < theParent.children.length; i++) {

let childElement = theParent.children[i];

childElement.addEventListener('click', doSomething, false);

}


function doSomething(e) {

let clickedItem = e.target.id;

console.log("Hello " + clickedItem);

}

В итоге этот подход позволял прослушивать события щелчка непосредственно в потомках. Единственным, что мне пришлось прописывать вручную, был один вызов слушателя событий, который параметризировался для соответствующего дочернего элемента в зависимости от того, где в цикле находился код:

childElement.addEventListener('click', doSomething, false);

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

Если у вас возникнет ситуация, в которой элементы будут разбросаны по DOM, не имея рядом общего родителя, использование этого подхода для массива HTML-элементов будет неплохим способом решения проблемы MEEC.

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

Глава 37. Заключение

Итак, вы справились! Вы наверняка читали без передышки и достигли конца. Как вы себя чувствуете, понимая, что до начала следующего сезона у вас не будет нового материала?

Если усердно читали книгу с самого начала, то согласитесь, что рассмотрели мы очень многое. Начали с такого:

А закончили куда более сложными, полезными и крутыми примерами кода, состоящими из гораздо большего числа строк.

Помните, что писать код легко, а вот писать элегантный код, решающий задачи, уже сложнее. Лучше всего это отражено в одном из моих любимых фильмов «Лицо со шрамом», где Тони Монтана произнес следующие слова (за точность не ручаюсь, поскольку зачастую его сложно понять — ну сами знаете, если смотрели фильм):

Эта книга целиком посвящена основам. Следующим шагом станет написание кода, изучение новых приемов и продолжение учебы. В этой книге описаны различные инструменты и приведены примеры их совместного использования для создания небольших программ. Вам осталось лишь вооружиться этими знаниями и применить их для создания более крутых и крупных проектов на JavaScript. Если вам такое по душе, ищите на моем сайте новые продвинутые уроки: http://www.kirupa.com.

Так что до скорой встречи, и не стесняйтесь черкнуть пару строк на kirupa@kirupa.com или на фейсбук и твиттер (@Kirupa). Как я уже говорил, мне нравится получать обратную связь от читателей, поэтому смело обращайтесь. Если у вас есть вопросы, не откладывайте и пишите на форум https://forum.kirupa.com.

И еще. Я понимаю, что выбор книг по JavaScript огромен. Поэтому хочу поблагодарить за то, что выбрали именно мою, позволив мне косвенно поприсутствовать в вашем редакторе кода.

Всех вам благ,

Загрузка...