Введение в всплывающие события

в 7:54, , рубрики: javascript, Веб-разработка, всплывающие события, перевод

Несмотря на то, что в конце концов я полностью использовал CSS для этого проекта, начиналось все с использования JavaScript и классов.

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

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

Окей, так в чем проблема?

Рассмотрим простой пример:

Предположим, имеется список кнопок. Каждый раз, когда я нажимаю на одну из них, она должна стать «активной». После повторного нажатия кнопка должна вернуться в исходное состояние.

Начнем с HTML:

<ul class="toolbar">
  <li><button class="btn">Pencil</button></li>
  <li><button class="btn">Pen</button></li>
  <li><button class="btn">Eraser</button></li>
</ul>

Я мог бы использовать стандартный JavaScript обработчик событий вроде такого:

for(var i = 0; i < buttons.length; i++) {
  var button = buttons[i];
  button.addEventListener("click", function() {
    if(!button.classList.contains("active"))
      button.classList.add("active");
    else
      button.classList.remove("active");
  });
}

Выглядит неплохо… Но работать он не будет. По крайней мере, не так, как мы этого ожидаем.

Замыкания победили

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

Для остальных же кратко объясню — функция обработчика замыкается на переменную button. Однако это переменная одна, и перезаписывается каждую итерацию.

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

Что нам нужно, так это отдельный контекст для каждой функции:

var buttons = document.querySelectorAll(".toolbar button");
var createToolbarButtonHandler = function(button) {
  return function() {
    if(!button.classList.contains("active"))
      button.classList.add("active");
    else
      button.classList.remove("active");
  };
};

for(var i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener("click", createToolBarButtonHandler(buttons[i]));
}

Намного лучше! А главное, правильно работает. Мы создали функцию createToolbarButtonHandle, которая возвращает обработчик события. Затем для каждой кнопки вешаем свой обработчик.

Так в чем проблема?

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

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

Но если мы имеем что-то подобное:

<ul class="toolbar">
  <li><button id="button_0001">Foo</button></li>
  <li><button id="button_0002">Bar</button></li>
  // ... еще 997 элементов  ...
  <li><button id="button_1000">baz</button></li>
</ul>

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

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

Event объект содержит некоторые данные о событии. В нашем случае нас интересует поле currentTarget. Из него мы получим ссылку на элемент, который был нажат:

var toolbarButtonHandler = function(e) {
  var button = e.currentTarget;
  if(!button.classList.contains("active"))
    button.classList.add("active");
  else
    button.classList.remove("active");
};

for(var i = 0; i < buttons.length; i++) {
  button.addEventListener("click", toolbarButtonHandler);
}

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

Однако мы все еще можем лучше.

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

Возможно, существует и другой подход?

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

Как же большинство из них работает?

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

  1. Фаза перехвата
  2. Событие возникает для целевого элемента
  3. Фаза всплывания

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

Событие создается снаружи документа и затем последовательно перемещается по DOM иерархии до target (целевого) элемента. Как только оно добралось до своей цели, событие тем же путем выбирается из DOM элемента.

Вот наш HTML шаблон:

<html>
<body>
  <ul>
    <li id="li_1"><button id="button_1">Button A</button></li>
    <li id="li_2"><button id="button_2">Button B</button></li>
    <li id="li_3"><button id="button_3">Button C</button></li>
  </ul>
</body>
</html>

Когда пользователь нажимает на кнопку А, событие путешествует таким образом:

Начало
| #document
| Фаза перехвата
| HTML
| BODY
| UL
| LI#li_1
| Кнопка А < — Событие возникает для целевого элемента
| Фаза всплывания
| LI#li_1
| UL
| BODY
| HTML
v #document

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

Всплывающие события

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

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

ul class="toolbar">
  <li><button class="btn">Pencil</button></li>
  <li><button class="btn">Pen</button></li>
  <li><button class="btn">Eraser</button></li>
</ul>

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

var toolbar = document.querySelector(".toolbar");
toolbar.addEventListener("click", function(e) {
  var button = e.target;
  if(!button.classList.contains("active"))
    button.classList.add("active");
  else
    button.classList.remove("active");
});

Теперь мы имеем намного более чистый код, и мы даже избавились от циклов! Заметьте однако, что мы заменили e.currentTarget на e.target. Причина кроется в том, что мы обрабатываем события на другом уровне.

e.target — фактическая цель события, то, куда оно пробирается через DOM, и откуда потом будет всплывать.
e.currentTarget — текущий элемент, который обрабатывает событие. В нашем случае, это ul.toolbar.

Улучшенные всплывающие события

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

<ul class="toolbar">
  <li><button class="btn"><i class="fa fa-pencil"></i> Pencil</button></li>
  <li><button class="btn"><i class="fa fa-paint-brush"></i> Pen</button></li>
  <li class="separator"></li>
  <li><button class="btn"><i class="fa fa-eraser"></i> Eraser</button></li>
</ul>

Упс! Теперь, когда мы кликаем на li.separator или иконку, мы добавляем ему класс .active. Как минимум, это нехорошо. Нам нужен способ фильтровать события так, чтобы мы реагировали на нужный нам элемент.

Создадим для этого небольшую функцию-помощника:

var delegate = function(criteria, listener) {
  return function(e) {
    var el = e.target;
    do {
      if (!criteria(el)) continue;
      e.delegateTarget = el;
      listener.apply(this, arguments);
      return;
    } while( (el = el.parentNode) );
  };
};

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

Мы можем использовать это так:

var toolbar = document.querySelector(".toolbar");
var buttonsFilter = function(elem) { return elem.classList && elem.classList.contains("btn"); };
var buttonHandler = function(e) {
  var button = e.delegateTarget;
  if(!button.classList.contains("active"))
    button.classList.add("active");
  else
    button.classList.remove("active");
};
toolbar.addEventListener("click", delegate(buttonsFilter, buttonHandler));

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

Подводя итоги

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

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

Функция-помощник для проверки удовлетворения объекта критериям в более унифицированном и функциональном виде. Вроде:

var criteria = {
  isElement: function(e) { return e instanceof HTMLElement; },
  hasClass: function(cls) {
    return function(e) {
      return criteria.isElement(e) && e.classList.contains(cls);
    }
  }
  // Больше критериев
};

Частичное использование помощника так же было бы не лишним:

var partialDelgate = function(criteria) {
  return function(handler) { 
    return delgate(criteria, handler);
  }
};

Оригинал статьи: Understanding Delegated JavaScript Events
(От переводчика: мой первый, судите строго.)

Счастливого кодинга!

Автор: missingdays

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js