Мы забыли про делегирование в JavaScript. Event delegation in React

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

О чем собственно речь? Зачем и почему?

Для начала давайте кратко обсудим:

  1. что есть событие;
  2. как происходит распространение;
  3. обработка 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>

В этом случае будет такой порядок:

  1. элемент div
  2. элемент body
  3. элемент html
  4. document
  5. window

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

В случае с перехватом событий работает все наоборот:

  1. window
  2. document
  3. элемент html
  4. элемент body
  5. элемент div

Задумывалось что событие можно будет обработать до того, как оно достигло целевого элемента (так решили в Netscape позже подхватили все современные браузеры).

В итоге мы имеем такую структуру распространения DOM-событий:

  1. window
  2. document
  3. элемент html
  4. элемент body // заканчивается фаза перехвата
  5. элемент div // целевая фаза
  6. элемент body // начинается фаза всплытия
  7. элемент html
  8. document
  9. 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 для профессиональных веб-разработчиков, автор: Николас Закас.

Спасибо большое за внимание. Если есть чем поделится или нашли какой-то недочет, может ошибку или просто есть вопрос, то пишите в комментариях. Буду рад любой обратной связи!

FavoriteLoadingДобавить в избранное
Posted in Без рубрики

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *