Всем привет. Статья о делегирование событий в JavaScript и реализация его в react.js.

О чем собственно речь? Зачем и почему?
Для начала давайте кратко обсудим:
- что есть событие;
 - как происходит распространение;
 - обработка DOM Level 2 с примером на JavaScript;
 
И в конце: почему не надо забывать об делегировании в React.
Событие
JavaScript с HTML взаимодействуют между собой за счёт событий (events). Каждое событие служит для того, чтобы сказать JavaScript’у о том, что в документе или окне браузера что-то произошло. Для того чтобы отловить эти события нам нужны слушатели (listeners), этакие обработчики, которые запускаются в случае возникновения события.
Распространение событий
Порядок. Решая проблему: как понять, какой части страницы принадлежит событие? Было реализовано два способа: в Internet Explorer — “всплытие событий”, а в Netscape Communicator — “перехват событий”.
Всплытие событий
В данном случае событие срабатывает у самого глубокого узла в дереве документа, после поднимается по иерархии до самого window.
<!DOCTYPE html>
<html>
  <head>
    <title>Some title</title>
  </head>
  <body>
    <div id="myDiv">Click Me</div>
  </body>
</html>В этом случае будет такой порядок:
- элемент div
 - элемент body
 - элемент html
 - document
 - window
 
Сейчас всплытие поддерживают все современные браузеры, хоть и с различной реализацией.
В случае с перехватом событий работает все наоборот:
- window
 - document
 - элемент html
 - элемент body
 - элемент div
 
Задумывалось что событие можно будет обработать до того, как оно достигло целевого элемента (так решили в Netscape позже подхватили все современные браузеры).
В итоге мы имеем такую структуру распространения DOM-событий:
- window
 - document
 - элемент html
 - элемент body // заканчивается фаза перехвата
 - элемент div // целевая фаза
 - элемент body // начинается фаза всплытия
 - элемент html
 - document
 - window
 
Делится эта схема на три фазы: фаза перехвата — событие можно перехватить до попадания на элемент, фаза цели — обработка целевым элементом и фаза всплытия — что бы выполнить какие-либо заключительные действия в ответ на событие.
Итак, переходим к обработке событий
Посмотрим типичный пример обработки события в JavaScript.
const btn = document.getElementById('myDiv')
btn.addEventListener("click", handler)
// some code
btn.removeEventListener("click", handler)Все бы нечего, но тут мы вспоминаем про наш любимы IE который подписывается на события с помощью attachEvent, а для удаления detachEvent. А еще можно подписываться на событие несколько раз. И не забываем что подписавшись анонимной функцией мы не имеем возможность отписаться.
Но мы же не г*внокодеры. Сделаем все по канону:
var EventUtil = {
  addHandler: function (elem, type, handler) {
    if (elem.addEventListener) {
      elem.addEventListener(type, handler, false)
    } else if (elem.attachEvent) {
      elem.attachEvent("on" + type, handler)
    } else {
      elem["on" = type] = hendler
    }
  },
  removeHandler: function (elem, type, handler) {
    if (elem.removeEventListener) {
      elem.removeEventListener(type, handler, false)
    } else if (elem.detachEvent) {
      elem.detachEvent("on" + type, handler)
    } else {
      elem["on" = type] = null
    }
  }
}Так хорошо, а как же объект event? Ведь в IE нет .target есть .srcElement, preventDefault? нет returnValue = false. Но нечего добавим пару методов:
var EventUtil = {
  addHandler: function (elem, type, handler) {
    if (elem.addEventListener) {
      elem.addEventListener(type, handler, false)
    } else if (elem.attachEvent) {
      elem.attachEvent("on" + type, handler)
    } else {
      elem["on" = type] = hendler
    }
  },
  getEvent: function (event) {
    return event ? event : window.event
  },
  getTarget: function (event) {
    return event.target || event.srcElement
  },
  preventDefault: function (event) {
    if (event.preventDefault) {
      event.preventDefault()
    } else {
      event.returnValue = false
    }
  },
  removeHandler: function (elem, type, handler) {
    if (elem.removeEventListener) {
      elem.removeEventListener(type, handler, false)
    } else if (elem.detachEvent) {
      elem.detachEvent("on" + type, handler)
    } else {
      elem["on" = type] = null
    }
  },
  stopPropagation: function (event) {
    if (event.stopPropagation) {
      event.stopPropagation()
    } else {
      event.cancelBubble = true
    }
  }
}И т.д. и т.п. и вот эти все танцы.
Хорошо мы молодцы, все проблемы решили, все ок. Правда код вышел довольно громоздким. А теперь представим, что нам нужно много подписок на множество элементов. Ух это займет не мало строк кода. Пример:
<ul>
<li id="id1">go somewhere</li>
<li id="id2">do something</li>
<li id="some-next-id">next</li>
</ul>
var item1 = document.getElementById('id1')
var item2 = document.getElementById('id2')
var itemNext = document.getElementById('some-next-id')
EventUtil.addHandler(item1, "click", someHandle)
EventUtil.addHandler(item2, "click", someHandle2)
EventUtil.addHandler(itemNext, "click", someHandle3)И так для каждого элемента, и надо удаление не забыть, работа с таргет и тому подобное
И тут к нам на помощь приходит делегирование событий (event delegation).
Все что нам надо это подключить один единственный обработчик к наивысшей точке в DOM-дереве:
<ul id="main-id"> // навешиваем id на родительский элемент
<li id="id1">go somewhere</li>
<li id="id2">do something</li>
<li id="some-next-id">next</li>
</ul>
var list = document.getElementById('main-id')
EventUtil.addHandler(list, "click", function(event) {
    event = EventUtil.getEvent(event)
    var target = EventUtil.getTarget(event)
    switch(target.id) {
       case "id1":
          // делаем что-то для элемента с id1
          break
       case "id2":
          // делаем что-то для элемента с id1
          break
       case "some-next-id":
          // делаем что-то для следующих элементов
          break
    }
})В итоге у нас только один обработчик в памяти, а для нужного действия можно использовать свойство id. Меньшее потребление памяти повышает общее быстродействие страницы в целом. Для регистрации обработчика событий требуется меньше времени и меньше обращений к DOM. Исключение разве что mouseover и mouseout, с ними все немного сложнее.
А теперь что насчёт React
Все что касается кросcбраузерности за нас уже все сделали ребята из facebook. Все наши обработчики событий получают экземпляр SyntheticEvent. Который заботится о нас повторно используя события из пула удаляя все свойства после вызова обработчика.
Хорошо.
Тем не менее лишний обработчик есть лишний обработчик. Несколько раз встречал, да и каюсь сам писал, такого рода код:
class Example extends React.Component {
  handleClick () {
    console.log('click')
  }
  render () {
    return (
      <div>
        {new Array(20).fill().map((_, index) =>
          <div
            key={index} // elem.id
            id={index} // elem.id
            onClick={() => console.log('click')}
          />
        )}
      </div>
    )
  }
}
В примере показан случай, когда есть какой-то лист с n-количеством элементов, а значит и с n-количеством регистраций обработчиков.
Запустим зайдем на страницу и проверим сколько обработчиков сейчас в деле. Для этого я нашёл не плохой скрипт:
Array.from(document.querySelectorAll('*'))
  .reduce(function(pre, dom){
    var clks = getEventListeners(dom).click;
    pre += clks ? clks.length || 0 : 0;
    return pre
  }, 0)
Работает в dev-tool хрома.
А теперь делегируем все это родительскому div элементу и ура, мы только что оптимизировали наше приложение в n=array.length раз. Пример код ниже:
class Example extends React.Component {
  constructor () {
    super()
    this.state = {
      useElem: 0
    }
  }
  handleClick (elem) {
    var id = elem.target.id
    this.setState({ useElem: id })
  }
  render () {
    return (
      <div onClick={this.handleClick}>
        {new Array(20).fill().map((_, index) =>
          <div
            key={index} // elem.id
            id={index} // elem.id
            useElem={index === this.state.useElem}
          />
        )}
      </div>
    )
  }
}Делегирование хороший инструмент для обработки большого количества подписок, а в случае с динамичным рендером и частых перерисовок просто незаменим. Пожалейте ресурсы пользователя, они не безграничны.
Статья написана на основе книги JavaScript для профессиональных веб-разработчиков, автор: Николас Закас.
Спасибо большое за внимание. Если есть чем поделится или нашли какой-то недочет, может ошибку или просто есть вопрос, то пишите в комментариях. Буду рад любой обратной связи!
Комментарии (8)

Aingis
13.09.2019 17:19Вообще-то React (и подобные ему) оптимизирует
onclickобработчики из коробки. Это легко проверить в девтулзах — там показываются обработчики на элементах.
P.S. Затрагивал тему ещё в 2012… Хабр всё помнит =).

EgorEr Автор
14.09.2019 01:55Перед тем как писать статью гуглил и толком нечего не нашёл. А по поводу оптимизации из коробки — когда проверял (в случае с мапом) регистрация обработчика все же происходит.

Dionis_mgn
14.09.2019 15:48регистрация обработчика все же происходит
Регистрация обработчиков. У вас там на каждую кнопку создаётся новая функция, которую ничего не связывает с другими кнопками и их обработчиками. Плохая практика, кстати, т.к. часто приводит к ненужным перерисовкам.

andres_kovalev
14.09.2019 02:15+1Зачем в 2019 все эти пляски с
attachEvent?
Как было сказано выше — про делегирование никто не забывал, просто зачастую разработчику при обработке события чаще нужны ассоциированные с элементом данные, а не сам элемент. Что предлагаете делать, если нужно увеличить счётчик на указанное на кнопке значение?
[ 10, 200, 3000].map( value => <button>+{ value }</button> )
Добавлять к элементу атрибут с значением и читать его в обработчике из DOM? По ид элемента определять значение используя некий map/array? Читать из innerHTML? Это все плохие практики. Нечто подобное гораздо проще и эффективнее:
[ 10, 200, 3000].map( value => <button on lick={ () => increment(value); }>+{ value }</button> )
DarthVictor
14.09.2019 16:56Добавлять к элементу атрибут с значением и читать его в обработчике из DOM?
Вообще data-атрибуты для этого и были придуманы.
Нечто подобное гораздо проще и эффективнее:
Откуда такая уверенность? Могу предположить, что эффективность как минимум зависит от числа кнопок. Ну и на ненативных элементах часто врубают react/jsx-no-bind.
andres_kovalev
14.09.2019 17:08+1Вообще data-атрибуты для этого и были придуманы.
Да, только это было придумано до React'а. Последний исповедует декларативный подход и предполагает минимум манипуляций с DOM и уж точно не с целью передавать через него данные.
Откуда такая уверенность?
Речь шла именно о приложении, разработанном с применением React.

DarthVictor
14.09.2019 17:46Речь шла именно о приложении, разработанном с применением React.
Так вы проверяли что такой способ эффективнее? Просто у меня вот был неудачный опыт с тормозами в списках и решения вроде байндинга в конструкторе создаваемого компонента или кастомного shoulComponentUpdate мне не показался элегантнее.
          
 
ThisMan
Все бы хорошо, но, кажется, про делегирование в реакте никто и не забывал...