JavaScript в браузере. Браузерные события

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

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

Добавление событий на страницу

Добавление в качестве атрибута onclick

Самый старый метод добавления обработчиков заключается в добавлении кода прямо а атрибут HTML-элемента. Для каждого типа события есть собственный атрибут, начинающийся с on-. Таким образом, обработчик события "клик по кнопке" можно добавить как:

<input type="button" value="Click me" onclick="alert('Thank you!\nClick me again, please!');" />

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

<input type="button" value="Click me" onclick="shout();" />

<script>
    var count = 1;
    function shout() {
        alert('Thank you!\nYou clicked me' + count + ' times.');
        count++;
    }
</script>

Добавление обработчика в HTML-код нарушает концепцию разделения логики приложения и внешнего вида, потому его не рекомендуется использовать.

Добавление в качестве свойства onclick

Атрибуты с on- доступны и как свойства.

<input type="button" value="Click me" />

<script>
    var count = 1;
    function shout() {
        alert('Thank you!\nYou clicked me' + count + ' times.');
        count++;
    }
    document.querySelector("input").onclick = shout;
</script>

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

Если были добавлены обработчики и в атрибут, и в свойство, то обработчик в свойстве перезапишет обработчик в атрибуте.

Если в аргументы обработчика передать this, то внутри обработчика можно получить ссылку на объект-источник события.

Добавление с помощью addEventListener

Самым универсальным способом добавления обработчика является использование addEventListener, первым параметром которого указывается тип события, а вторым - обработчик:

<input type="button" value="Click me" />

<script>
    var count = 1;
    function shout() {
        alert('Thank you!\nYou clicked me' + count + ' times.');
        count++;
    }
    document.querySelector("input").addEventListener('click', shout);
</script>

Добавление каждого нового обработчика не удаляет предыдущие, в результате чего можно добавлять сколько угодно обработчиков в любом месте программы. Добавленные через addEventListener обработчики не влияют на обработчики, добавленные через on-

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

document.querySelector("input").removeEventListener('click', shout);

Удаление возможно только того обработчика, который был ранее добавлен. Т.е. если обработчич был добавлен, как анонимная функция, то удалить его через removeEventListener не получится.

В IE8 и ниже вместо addEventListener/removeEventListener используется пара attachEvent/detachEvent:

document.querySelector("input").attachEvent('onclick', shout);
document.querySelector("input").detachEvent('onclick', shout);

Тип события в этом случае передается с on. Обработчики, добавленные таким образом, не получают this.

Порядок обработки событий

Основной поток кода в JavaScript называется главным потоком. Он выполняется непрерывно, пока не останется кода для выполнения, либо пока он не будет прерван, например, модальным окном. Таким образом вновь возникшее событие не обрабатывается мгновенно в параллельном потоке, а попадает в очередь событий и ее обработчик ждет возможности появления "окна" в основном потоке для своего выполнения. В очередь могут попадать несколько событий сразу, например, когда событие click следует сразу же за onmouseup. Если функция, выполняемая в данный момент, очень "тяжелая", то окна может не быть в течение некоторого времени, и событие не будет обработано быстро. Таким образом интерфейс будет "подтормаживать".

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

<input type="button" id="button1" value="Click 1" />
<input type="button" id="button2" value="Click 2" />

<script>
    function callback1 () {
        console.log('start callback 1');
        button2.click();
        console.log('end callback 1');
    }
    function callback2 () {
        alert('callback 2');
    }
    button1.onclick = callback1;
    button2.onclick = callback2;
</script>

Обработчик события для button1 не завершится до тех пор, пока не завершится обработчик для button2.

Чтобы сохранить последовательность выполнения обработчиков событий можно:

Объект события

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

Все объекты события имеют базовые свойства:

<input type="button" value="Click me" />

<script>
    document.querySelector("input").addEventListener('click', function (event) {
        console.log('Type:' + event.type);
        console.log('Target:' + event.currentTarget);
    });
</script>

В IE8 и ниже объект события не передается в качестве аргумента, а создается в глобальной области видимости как window.event.

Всплытие и перехват

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

<div>
    <span>
        <input type="button" value="Click me" />
    </span>
</div>

<script>
    document.querySelector("div").addEventListener('click', callb);
    document.querySelector("span").addEventListener('click', callb);
    document.querySelector("input").addEventListener('click', callb);
    function callb(event) {
        console.log('Clicked on: ' + event.currentTarget.tagName);
    }
</script>

При клике на кнопку в консоли последовательно отобразится информация о клике по INPUT, затем по SPAN и затем DIV.

Вплытие — это механизм, при котором событие сначала вызывает обработчик у объекта-источника, затем у его родителя, затем у родителя-родителя и т.д. до самого "верха" — до document.

При всплытии у каждого обработчика будет свой currentTargetthis), но всегда одинаковый target.

Прекращение всплытия

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

<div>
    <span>
        <input type="button" value="Click me" />
    </span>
</div>

<script>
    document.querySelector("div").addEventListener('click', callb);
    document.querySelector("span").addEventListener('click', callb);
    document.querySelector("input").addEventListener('click', callb);
    function callb(event) {
        console.log('Clicked on: ' + event.currentTarget.tagName);
        event.stopPropagation();
    }
</script>

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

Погружение

Помимо всплытия стандарт описывает еще самую первую фазу — погружение. Т.е. в процессе возникновения событие сначала "выстреливает" для window и опускается "вниз" до элемента-источника (фаза погружения), обрабатывается обработчиком, навешанным на элемент-источник (фаза цели), а затем опять "поднимается" к window (фаза всплытия).

Обработка события на фазе погружения на практике используется редко. Свойства on- не знают об этой фазе. Для addEventListener можно указать на необходимость перехвата события на фазе погружения с помощью третьего необязательного аргумента phase, установив его в true:

<input type="button" value="Click me" />

<script>
    document.body.addEventListener('click', callb, true);
    document.body.addEventListener('click', callb, false);
    function callb(event) {
        console.log('Event phase: ' + event.eventPhase);
    }
</script>

Делегирование событий

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

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

Делегирование удобно использовать для обработки разных событий для однотипных элементов. В этом случае элементы удобно различать по data-атрибуту.

Приём проектирования "поведение"

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

Действия браузера по умолчанию

Для некоторых событий есть поведение браузера по умолчанию. По клике на ссылку происходит переход по указанному в href url, по клику на submit-кнопке формы она отправляется на сервер и т.д. Иногда необходимо отменить это действие по умолчанию. Сделать это можно двумя способами:

Возможно "подавление" и событий. Например, клик по элементу ввода формы передает фокус элементу. Если "подавить" обработку onmousedown, то и фокус элемент не получит. Хотя все еще будет возможно получить фокус с помощью Tab.

Генерация событий на элементах

События можно не только перехватывать и обрабатывать, но и генерировать. Для современных браузеров (в т.ч. IE12 и выше) новое событие можно создать с помощью конструктора Event. Привязка вновь созданного события к DOM-элементу осуществляется с помощью метода dispatchEvent. Например, эмуляция клика по телу элемента может выглядеть как:

var event = new Event('click', { bubbles: true, cancelable: true});
document.body.dispatchEvent(event);

Если в обработчике события присутствует preventDefault, то программная генерация события вернет false.

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

Сгенерированные программно события имеют свойство isTrusted равное false.

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

Можно генерировать более специфические события с помощью встроенных конструкторов:

Использование специфических конструкторов позволяет указать свойство события в списке флагов (второй параметр конструктора), чего нельзя было сделать для простого Event.

Собственные события можно генерировать как с помощью Event, так и с помощью конструктора CustomEvent.

Мышь: клики, кнопка, координаты

Cобытия разного типа могут иметь специфические свойства. Свойства объекта события "клик мышью":