Мы забыли про делегирование в 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 для профессиональных веб-разработчиков, автор: Николас Закас.


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

Источник: https://habr.com/ru/post/467361/


Интересные статьи

Интересные статьи

В приложениях Lingualeo сложился довольно редкий кейс. Их создали до того, как появились кроссплатформенные технологии, но через несколько лет туда добавили модули на React Native. К...
Добрый день. Недавно мне необходимо было разобраться со службой трассировки Windows. Эта служба появилась еще в Windows 2000, однако статей по этой службе в интернете оказалось крайне...
VUE.JS - это javascript фрэймворк, с версии 18.5 его добавили в ядро битрикса, поэтому можно его использовать из коробки.
В современных фронтенд-приложениях технология CSS-in-JS пользуется определённой популярностью. Всё дело в том, что она даёт разработчикам механизм работы со стилями, который удобнее обычного CSS....
Как широко известно, с 1 января 2017 года наступает три важных события в жизни интернет-магазинов.