Це дʼелегантно! Або що можна робити з bubbling та capturing
Дана стаття розрахована на початківців і має на меті познайомити їх з основними ідеями, описаними в матеріялі. Майте бога в серці, не намагайтесь звернути мою увагу на те, що я не написав про те, чи про інше. Цей матеріял містить рівно те і рівно стільки, скільки я планував написати. Дякую.
Веброзробка в основі своїй покликана для створення користувацьких інтерфейсів для взаємодії користувача з вашим продуктом. І складаються ці інтерфейси з різноманітних елементів, однак саме інтерфейсами роблять їх саме різноманітні кнопочки, інпути та інші елементи, які цю саму взаємодію й забезпечують. А інформацію про саму взаємодію невпинно та сумлінно переносять сотні DOM-подій, що створюються бравзером у відповідь на усі дії користувача.
Тицьнув мишкою? Тримай подію. Написав непристойність у полі введення повідомлення? Тримай цілий мішок різноманітних подій. Це все відбувається в реальному часі, і бравзер створює просто нереальну кількість подій у відповідь на кожнісіньку дію користувача. Однак ми не помічаємо цього, бо на більшість цих подій не створено обробники, та й самі події це частина DOM-інтерфейсу, робота якого надійно прихована від сторонніх очей під капотом.
І багато чого цікавого відбувається під цим капотом, що може вплинути на швидкодію вашої сторінки, адже наламати дров можна дуже просто. Найбільш банальний приклад — призначати анонімний обробник на кожен елемент окремо:
document.querySelector('button').forEach(button => { button.addEventListener('click', event => {…}) })
Якщо у вас буде 1 000 кнопок, то відповідно й створиться 1 000 екземплярів обробника. І воно хоч і не те, аби багацько памʼяті займало, але, як-то кажуть, байт до байта, то й гігабайт назбирається.
Або ще можна просто назбирати зайвого сміття, буквально — якщо не прибирати правильно обробники подій, то вони будуть просто нагромаджуватися в памʼяті, спричиняючи так звані протікання памʼяті, коли там залишаються дані, які неможливо прибрати.
button.addEventListener('click', () => {…}); document.body.innerHTML = '';
Ну і ще є кілька неприємних наслідків бездумного навішування обробників на усе, що клікається, але цю тему ви можете дослідити самотужки, до чого я вас усіляко й заохочую.
А от нині давайте розберемо один підхід, який дозволяє уникнути вищезгаданих проблем, а саме — делегацію подій.
Для початку просте пояснення, що коїться в момент, до прикладу, натискання кнопки. Отже, бравзер чудово знає, хто відповідальний за подію, і створює обʼєкт DOM Event, однак замість того, аби обмежити його видимість лише цим елементом, він прицільно кидає цю подію від самісінького вершечка DOM-дерева аж до нашої кнопки. І обʼєкт зі свистом пролітає крізь усі батьківські елементи, кожен з яких має нагоду на нього відреагувати, так би мовити, «зловити», або ж capture. І так, ця фаза називається capturing phase.
Якщо коротко, можна уявити собі це ніби ви живете на першому поверсі хмарочоса, а на сотому поверсі працює піцерія, з якої ви замовили собі доставку. Доставлять вам попоїсти ліфтом, однак зважайте, що на кожному поверсі ліфт можуть зупинити і поцікавитись, а що ж там в коробці їде і куди. І кожен буде знати, що це саме ваше замовлення, бо на коробці писатиме великими літерами ваше імʼя. Що прикро — на кожному з поверхів зможуть перехопити вашу піцу, використавши простеньку кодову фразу:
event.stopPropagation()
І тоді ви свого обіду не дочекаєтесь.
Якщо ж таки дочекаєтесь, то тоді розпочнеться наступна фаза — bubbling phase, або ж фаза спливання події, яку можна для зручности називати підняттям події. Це якби ви робили замовлення не телефоном, а передавали записку тим же ліфтом. І так само на кожному поверсі вашу записку можуть перехопити і з цікавістю перечитати. Або й не пропустити далі за допомогою тієї самої кодової фрази.
Єдиний момент, що цей приклад трохи притягнутий за вуха, бо на ділі спочатку спрацьовує фаза спуску події, а вже потім — підйому. Тут швидше підійшла би аналогія з, наприклад, Еціо Авдиторе чи Едвардом Кенвеєм, що виконує стрибок віри до своєї жертви, а потім, після виконання завдання, намагається втекти від сторожі назад на вежу.
Так от. Приклад індивідуального обробника в цьому хмарочосі полягає в тому, що для вас особисто буде зарезервовано іменних піцайолу, касира та курʼєра. Так собі радість, зважаючи, що ви можете ту піцу й не замовити ніколи аж до самої смерті.
Тому варто цей процес оптимізувати, вірно? І замість обробляти усі замовлення на особистому рівні чи не краще це робити якось узагальнено? Звичайно ж.
Приблизно так я уявляю собі capturing phase з точки зору обʼєкта події
І тут нам в нагоді стане такий підхід, як делегування подій. Він полягає в тому, щоб обробляти події від однотипних елементів на вищому рівні, не привʼязуючись до самого елемента. І розглянемо ми це на прикладі картки в списку справ.
Всі знають, що таке TODO-list, тому не вдаватимусь в деталі. Зазначу лише, що картки там зазвичай структурно і функціонально однакові і вміють робити одне й те саме. Тому нас не дуже цікавить, яка саме кнопка ініціювала подію, нас цікавить, як на це правильно зреагувати.
Ось дуже спрощений приклад картки:
<li> <button>Редагувати</button> <button>Видалити</button> <button>Завершити</button> </li>
І карток може бути прямо багацько. І кнопок, відповідно, теж. Перше, що нам прийде в голову, це зробити щось таке:
<li id=":id"> <button id="edit:id">Редагувати</button> <button id="delete:id">Видалити</button> <button id="complete:id">Завершити</button> </li> <script> document.querySelectorAll('li').forEach(li => { const id = li.id; document.getElementById(`edit:${id}`, () => {…}) document.getElementById(`delete:${id}`, () => {…}) document.getElementById(`complete:${id}`, () => {…}) }); </script>
Чи це буде працювати? Абсолютно так! Чи буде це створювати проблеми, розглянуті вище? Так само, абсолютно так! Тож, як їх уникнути? А дуже просто.
Для цього нам потрібно додати обробник подій на батьківський елемент, і вже там, централізовано, обробляти ті самі кліки. Я відразу покажу фінальний код, а тоді розберемося, що до чого:
<ul id="todoList"> … <li id=":id"> <button data-action="edit">Редагувати</button> <button data-action="delete">Видалити</button> <button data-action="complete">Завершити</button> </li> … </ul> <script> function edit() {} function remove() {} function complete() {} document.getElementById('todoList').addEventListener('click', event => { const action = event.target.dataset.action; const todoItem = event.target.closest('li'); switch (action) { case 'edit': edit(todoItem); return; case 'delete': remove(todoItem); return; case 'complete': complete(todoItem); return; default: return; } }); </script>
- Обробник тепер на одному елементі — батьківському ul.
- Для визначення конкретного елемента списку мені достатньо узяти батьківський елемент кнопки, що була натиснута.
- Тепер мене не цікавить, яка саме кнопка була натиснута, мене цікавить лише її дія, action, та батьківський елемент списку, з яким ми й будемо щось робити.
- Логіка роботи з елементом тепер в окремих функціях замість безпосередньо в обробниках, що дозволяє відокремити її від поведінки подій в принципі, і перевикористати деінде.
- Сам обробник не залежить на конкретний елемент списку, для нього неважливо, скільки тудушок зараз є, чи видалили, а чи додали якийсь елемент. Він суто приймає сигнал і виконує дії з ініціатором цього сигналу.
Цей обробник працює у фазі bubbling, і це є найпоширеніший підхід, адже дозволяє додатково обробляти події на нижчих рівнях і, до прикладу, не пропускати цю подію наверх за певних обставин.
До речі, якщо у нас в кнопці буде якийсь інший елемент, хай навіть іконка, то якщо клікнуть в неї, то event.target буде не кнопка, а цей елемент. Аби цьому запобігти, можна використати, наприклад, наступний CSS:
<button> <svg class="icon"></svg> </button> <style> svg { pointer-events: none; } </style>
Елемент з цією властивістю перестає реагувати на події курсора повністю.
У фазі capturing делегація подій теж цілком собі можлива, різниця лише в тому, що ми можемо зупинити подію на верхньому рівні і не пропускати її нижче.
Дуже сподіваюсь, що вдалося привідкрити завісу таємничости над делегацією подій та пояснити практичне їхнє застосування. Дякую за увагу!
Усі статті, обговорення, новини про Front-end — в одному місці. Підписуйтеся на телеграм-канал!
20 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів