DOM и события — Примеры

Пример 1 — Слушатель на каждую строку, собранную из HTML-строки

В разметке ниже есть список результатов, а скрипт строит каждую строку, присваивая строку и навешивая на неё собственный обработчик клика. Скажите, что ломается по мере роста списка, и перепишите скрипт так, чтобы слушатель был один и работал через делегирование.

<ul id="results"></ul>
<script src="app.js"></script>
function render(rows) {
  const list = document.querySelector("#results");
  list.innerHTML = "";
  rows.forEach((row) => {
    list.innerHTML += `<li>${row.name} <button>Удалить</button></li>`;
    list.querySelectorAll("button").forEach((button) => {
      button.addEventListener("click", () => removeRow(row.id));
    });
  });
}

Решение

const list = document.querySelector("#results");

list.addEventListener("click", (event) => {
  const button = event.target.closest("button[data-id]");
  if (!button) return;
  removeRow(button.dataset.id);
});

function render(rows) {
  const items = rows.map((row) => {
    const item = document.createElement("li");
    item.textContent = row.name;

    const button = document.createElement("button");
    button.textContent = "Удалить";
    button.dataset.id = row.id;
    item.append(button);

    return item;
  });
  list.replaceChildren(...items);
}

На каждой итерации исходный вариант навешивает обработчик на очередную строку и пересобирает весь список заново, дописывая в innerHTML; работа растёт как квадрат размера списка, а пользовательский текст разбирается как разметка. События всплывают, поэтому один слушатель на контейнере с проверкой event.target обработает и все строки, включая добавленные позже. Настоящие узлы с textContent задают имя текстом, а не HTML. Вставка готовых узлов один раз заставляет браузер пересчитать раскладку однократно вместо раза на строку.

Пример 2 — Слушатель на window, который никогда не снимают

Модуль регистрирует обработчик resize при открытии панели. Стоит открыть панель заново — и обработчик регистрируется ещё один. Объясните, что при этом удерживается, и привяжите время жизни слушателя к панели.

function openPanel() {
  window.addEventListener("resize", () => {
    document.querySelector("#panel").style.width = window.innerWidth / 3 + "px";
  });
}

// открыть и закрыть панель четыре раза -> четыре обработчика, все ещё работают

Решение

let onResize = null;

function openPanel() {
  const panel = document.querySelector("#panel");
  onResize = () => {
    panel.style.width = window.innerWidth / 3 + "px";
  };
  window.addEventListener("resize", onResize);
}

function closePanel() {
  if (onResize) window.removeEventListener("resize", onResize);
  onResize = null;
}

Пока слушатель прикреплён, он держит своё замыкание живым, а каждый обработчик здесь замыкается на элемент панели, которого после закрытия уже нет, поэтому удерживаемый набор растёт с каждым открытием. Снять слушателя позже позволяет только именованная ссылка на функцию: анонимную стрелочную функцию для removeEventListener не найти. С удалением элемента его слушатели уходят вместе с ним; однако window и document переживут любую панель, поэтому всё, что навешено на них, придётся снимать явно.