Способи спрощення обробників подій

7 хв. читання

Ми маємо справу з обробниками подій щоразу, коли користувач клікає на певний елемент, створює фокус клавіатурою чи вводить текст в поле форми. Дуже важливо не перевантажувати обробники зайвим кодом і, власне, не створювати зайвих обробників.

Почнемо з простого прикладу: декількох елементів з властивістю draggable="true". Ми хочемо сповістити користувача про колір елемента, який вони перетягують:

<section>
  <div id="red" draggable="true">
    <span>R</span>
  </div>
  <div id="yellow" draggable="true">
    <span>Y</span>
  </div>
  <div id="green" draggable="true">
    <span>G</span>
  </div>
</section>

<p id="dragged">Drag a box</p>

Інтуїтивний спосіб

Якщо ви тільки почали вивчення JavaScript, то, імовірно, створили б по обробнику для кожного елемента. Це поширений підхід, адже він найбільш очевидний. Нам потрібна особлива поведінка для кожного елемента, тому створюємо окремі обробники:

document.querySelector('#red').addEventListener('dragstart', evt => {
  document.querySelector('#dragged').textContent = 'Dragged red';
});

document.querySelector('#yellow').addEventListener('dragstart', evt => {
  document.querySelector('#dragged').textContent = 'Dragged yellow';
});

document.querySelector('#green').addEventListener('dragstart', evt => {
  document.querySelector('#dragged').textContent = 'Dragged green';
});

Забираємо повторюваний код

Можна помітити, що всі обробники дуже схожі за поведінкою: кожна функція виводить певний текст. Винесемо спільний код до однієї допоміжної функції:

function preview(color) {
  document.querySelector('#dragged').textContent = `Dragged ${color}`;
}

document
  .querySelector('#red')
  .addEventListener('dragstart', evt => preview('red'));
document
  .querySelector('#yellow')
  .addEventListener('dragstart', evt => preview('yellow'));
document
  .querySelector('#green')
  .addEventListener('dragstart', evt => preview('green'));

Стало трохи чистіше, однак нам досі треба декілька функцій та слухачів подій.

Переваги об'єкта Event

Об'єкт Event — основний помічник у спрощенні слухачів. При виклику обробника події першим аргументом передається об'єкт Event. Він містить деякі дані для опису події, що виникла, на зразок часу її виникнення.

Щоб спростити наш код, ми можемо використати властивість evt.currentTarget, де currentTarget посилається на елемент, саме до якого і приєднаний слухач. В нашому прикладі це буде один з кольорових блоків.

const preview = evt => {
  const color = evt.currentTarget.id;
  document.querySelector('#dragged').textContent = `Dragged ${color}`;
};

document.querySelector('#red').addEventListener('dragstart', preview);
document.querySelector('#yellow').addEventListener('dragstart', preview);
document.querySelector('#green').addEventListener('dragstart', preview);

Отримали одну функцію замість чотирьох. Ми можемо повторно використовувати ту саму функцію як обробника подій, а evt.currentTarget.id отримуватиме різні значення: залежно від елемента, який викликав подію.

Використовуємо сплиття

Внесемо остаточні зміни в наш код і зменшимо його. Замість приєднувати обробник до кожного елемента, ми можемо приєднати єдиний обробник для <section>: батьківського елемента для всіх інших кольорових блоків.

Подія виникає на елементі (в нашому випадку на одному з блоків), з яким взаємодіє користувач. Однак на цьому все не зупиняється. Браузер переходить ієрархією батьківських елементів, викликаючи їх слухачів подій. Це продовжуватиметься, поки не буде досягнуто кореневого елемента в документі (тегу <body>). Такий процес називається «сплиття», оскільки подія підіймається деревом елементів, подібно до бульбашки.

Способи спрощення обробників подій

Коли ми приєднуємо слухача до <section>, подія, викликана на дочірньому елементі, спливає до батьківського. Ми також можемо отримати певні переваги від властивості evt.target, що посилається на елемент, який ініціював подію (один з блоків в нашому прикладі), а не елемент, до якого приєднаний слухач (у нас це <section>).

const preview = evt => {
  const color = evt.target.id;
  document.querySelector('#dragged').textContent = `Dragged ${color}`;
};

document.querySelector('section').addEventListener('dragstart', preview);

Нам вдалось зменшити кількість слухачів подій до одного. Якби у нас був складніший код, ефект був би ще помітнішим. За допомогою об'єкта Event і сплиття можна суттєво спростити код обробників подій.

Як щодо події click

evt.target добре працює в подіях на зразок dragstart та change, де ми маємо справу лише з невеликим числом елементів, які отримують фокус чи змінюють інпут.

Зазвичай нам треба якось реагувати на кліки користувача, тому зупинимось на події click детальніше. Ви можете обробляти click для будь-якого елемента в документі: від великих div до невеличких span.

Зробимо так, щоб draggable-блоки тепер реагували на подію click.

<section>
  <div id="red" draggable="true">
    <span>R</span>
  </div>
  <div id="yellow" draggable="true">
    <span>Y</span>
  </div>
  <div id="green" draggable="true">
    <span>G</span>
  </div>
</section>

<p id="clicked">Clicked a box</p>
const preview = evt => {
  const color = evt.target.id;
  document.querySelector('#clicked').textContent = `Clicked ${color}`;
};

document.querySelector('section').addEventListener('click', preview);

Якщо ви тестуватимете цей код, зверніть увагу, що іноді назва кольору не приєднується до рядка Clicked при кліку на блок. Усе тому, що кожен блок містить <span>, який обробляє клік замість нашого draggable-елементу <div>. Оскільки у <span> не визначено id, властивість evt.target.id поверне пустий рядок.

Однак нас турбують все ж таки кольорові блоки. Оскільки ми клікаємо десь всередині блоку, нам необхідно знайти батьківський елемент. Для цього використаємо element.closest(), щоб знайти батьківський елемент, найближчий до обраного.

const preview = evt => {
  const element = evt.target.closest('div[draggable]');
  if (element != null) {
    const color = element.id;
    document.querySelector('#clicked').textContent = `Clicked ${color}`;
  }
};

Тепер можемо використовувати одного слухача для подій click. Якщо element.closest() повертає null, то ми натиснули десь за межами кольорового елемента, тому ігноруємо таку подію.

Більше прикладів

З ними ви ще краще зрозумієте переваги єдиного обробника.

Списки

Поширений трюк, коли ми маємо список елементів, які додаються динамічно за допомогою JavaScript. Якби ми мали слухача подій для кожного елемента, то довелося б мати справу з ними щоразу, коли генерується новий елемент.

<div id="buttons-container"></div>
<button id="add">Add new button</button>
let buttonCounter = 0;
document.querySelector('#add').addEventListener('click', evt => {
  const newButton = document.createElement('button');
  newButton.textContent = buttonCounter;
  
  // Створюємо нового слухача при кожному кліку на "Add new button"
  newButton.addEventListener('click', evt => {

    // При кліку логуємо номер кнопки 
    document.querySelector('#clicked').textContent = `Clicked button #${newButton.textContent}`;
  });

  buttonCounter++;

  const container = document.querySelector('#buttons-container');
  container.appendChild(newButton);
});

За допомогою сплиття ми можемо використовувати єдиного слухача подій на контейнер. Якщо ми створюємо багато елементів у застосунку, кількість слухачів зменшується від n до двох.

let buttonCounter = 0;
const container = document.querySelector('#buttons-container');
document.querySelector('#add').addEventListener('click', evt => {
  const newButton = document.createElement('button');
  newButton.dataset.number = buttonCounter;
  buttonCounter++;

  container.appendChild(newButton);
});
container.addEventListener('click', evt => {
  const clickedButton = evt.target.closest('button');
  if (clickedButton != null) {
        // При кліку логуємо номер кнопки 
    document.querySelector('#clicked').textContent = `Clicked button #${clickedButton.dataset.number}`;
  }
});

Форми

Уявіть, що ми маємо справу з формою, яка містить багато інпутів, а ми хочемо зібрати всі відповіді користувача в єдиному об'єкті.

<form>
  <label>Name: <input name="name" type="text"/></label>
  <label>Email: <input name="email" type="email"/></label>
  <label>Password: <input name="password" type="password"/></label>
</form>
<p id="preview"></p>
let responses = {
  name: '',
  email: '',
  password: ''
};

document
  .querySelector('input[name="name"]')
  .addEventListener('change', evt => {
    const inputElement = document.querySelector('input[name="name"]');
    responses.name = inputElement.value;
    document.querySelector('#preview').textContent = JSON.stringify(responses);
  });
document
  .querySelector('input[name="email"]')
  .addEventListener('change', evt => {
    const inputElement = document.querySelector('input[name="email"]');
    responses.email = inputElement.value;
    document.querySelector('#preview').textContent = JSON.stringify(responses);
  });
document
  .querySelector('input[name="password"]')
  .addEventListener('change', evt => {
    const inputElement = document.querySelector('input[name="password"]');
    responses.password = inputElement.value;
    document.querySelector('#preview').textContent = JSON.stringify(responses);
  });

А тепер варіант з одним слухачем на батьківському елементі <form>:

let responses = {
  name: '',
  email: '',
  password: ''
};

document.querySelector('form').addEventListener('change', evt => {
  responses[evt.target.name] = evt.target.value;
  document.querySelector('#preview').textContent = JSON.stringify(responses);
});
Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.8K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація