Перейти к основному содержимому
Блокчейн41ГЛАВА 41DOM и события: страница начинает отвечатьbodyulformbuttonclick ↑ всплывает

DOM и события: страница начинает отвечать

Прочитано0%
Квизы0/4
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

Страница описана и оформлена, но она мертва. Нажатие на кнопку ничего не делает, введённая сумма никуда не уходит, баланс не обновляется.

Оживляет её тот же язык, на котором мы четырнадцать глав писали скрипты. Только выполняется он не в Node, а в браузере, и у него есть то, чего в Node нет: страница, к которой можно обращаться из кода.

Эта глава — про три вещи: как найти элемент, как его изменить и как поймать действие пользователя. И в конце — измерение, из которого растёт следующая глава: почему делать это руками перестают, как только интерфейс становится больше десяти элементов.

Дерево документа​

Когда браузер разобрал разметку, он держит в памяти дерево объектов: у каждого элемента страницы есть соответствующий объект, у него — родитель, потомки, атрибуты и содержимое.

Это дерево и есть то, что видно на экране. Изменили объект — изменилась страница, мгновенно. Никакой «перезагрузки» для этого не нужно.

Важное следствие: исходный файл разметки и дерево — разные вещи. Файл прочитан один раз; дальше живёт дерево, и добавленные из кода элементы в файле не появятся никогда.

Найти элемент​

── найти ──
querySelector("#t") → H1, текст "Хранилище"
querySelectorAll(".op") → 3 шт., тип NodeList
есть ли у него .map → false
[...список].map даёт → вклад, вывод, вклад
Нажми на часть выражения
Совет

Селектор в поиске — тот же, что в оформлении. Это значит, что классы, придуманные для внешнего вида, годятся и для поиска. Но для поиска из кода обычно заводят отдельные имена — иначе правка оформления ломает логику.

Изменить: текст или разметка​

У элемента есть два способа задать содержимое, и разница между ними — вопрос безопасности, а не удобства.

── одна и та же строка от пользователя, два способа вставки ──
строка: <img src=x onerror='document.title="взломано"'>Баланс: 100
innerHTML → заголовок вкладки стал: "взломано"
textContent → на странице осталось: "<img src=x onerror='document.title="взло…"
ОДНА СТРОКА ОТ ПОЛЬЗОВАТЕЛЯ, ДВА СПОСОБА ВСТАВКИ<img src=x onerror=… >Баланс: 100innerHTMLстрока разбирается как разметкакартинка не загрузилась — сработал кодзаголовок вкладки стал «взломано»textContentстрока остаётся строкойна странице виден сам текст с угловыми скобкамивыполнять нечеговсё, что пришло от пользователя или из сети, вставляют текстом — это не осторожность, а правилоадрес кошелька, имя, комментарий: любое из этого может оказаться разметкой с кодом внутри
Разметка разбирается и выполняется, текст остаётся текстом: строка от пользователя должна попадать вторым путём

Первый способ разбирает строку как разметку. В прогоне это выполнилось по-настоящему: картинка не загрузилась, сработал обработчик ошибки, и заголовок вкладки сменился.

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

Второй способ вставляет строку как текст. Угловые скобки видны на экране, выполнять нечего.

Правило без исключений: всё, что не написано вами в файле, вставляется текстом. Разметку из строки собирают только тогда, когда строку собрали вы сами, из проверенных кусков.

Отвечено вопросов: 0 из 2
Чем опасна вставка строки через innerHTML?
querySelectorAll вернул список, а .map на нём не работает. Почему?
Отвечено верно: 0 из 2

Создать и вставить​

const row = document.createElement("li");
row.className = "op";
row.textContent = `${who} — ${amount} ETH`;
row.dataset.id = id;
list.append(row);
Нажми на часть выражения

Строка из шаблона с подстановкой (${who}) здесь безопасна: она попадает в текст, а не в разметку.

События: поймать действие​

button.addEventListener("click", (event) => {
console.log("нажали", event.target);
});
Нажми на часть выражения

Всплытие: событие идёт вверх​

Событие не остаётся на элементе, где произошло: оно поднимается по дереву до самого верха, и по дороге срабатывают все обработчики.

── всплытие: нажали кнопку, слушает контейнер ──
сама кнопка 1 тоже отреагировала
контейнер поймал нажатие на кнопке 1
контейнер поймал нажатие на кнопке 2
добавили третью кнопку уже после навешивания обработчика:
контейнер поймал нажатие на кнопке 3
СОБЫТИЕ ВСПЛЫВАЕТ ОТ КНОПКИ ВВЕРХконтейнер — обработчик здесь123третья добавленауже потомчто вывел прогонкнопка 1: сработали обакнопка 2: сработал контейнеркнопка 3: сработал контейнеробработчик один на всеходин обработчик на контейнере ловит нажатия и на тех кнопках, которых в момент навешивания не былокого именно нажали — видно из события; так делают списки, где строки появляются и исчезают
Обработчик на контейнере ловит нажатия и на тех кнопках, которых при его навешивании ещё не было

Порядок в первых двух строках не случаен: сначала сработал обработчик на самой кнопке, потом на контейнере. Событие шло снизу вверх.

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

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

Три причины так делать:

  • обработчик один, а не сто — меньше памяти и меньше работы при построении списка;
  • работает для будущих элементов: третья кнопка в прогоне добавлена уже после навешивания, и её нажатие поймалось;
  • не надо снимать обработчики при удалении строк — они и не навешивались.

Форма без перезагрузки​

── отправка формы перехвачена ──
данные формы: {"amount":"5"}
страница не перезагрузилась, значение поля на месте: "5"
form.addEventListener("submit", (event) => {
event.preventDefault();
const data = Object.fromEntries(new FormData(event.target));
deposit(data.amount);
});

Три детали, каждая из которых важна.

Слушают событие отправки у формы, а не нажатие у кнопки. Форму отправляют и клавишей ввода в поле — обработчик на кнопке этого не поймает.

Отмена поведения по умолчанию обязательна. Без неё браузер уходит на другой адрес, и всё введённое исчезает. Это самая частая причина «нажал — страница моргнула и всё пропало».

Данные берут из формы целиком. Не собирают по одному полю через поиск: имена уже написаны в разметке, и сбор одной строкой не разъезжается с ней.

Отвечено вопросов: 0 из 2
Почему один обработчик на контейнере лучше ста на кнопках?
Нажали кнопку в форме, страница моргнула, введённое пропало. Что забыли?
Отвечено верно: 0 из 2

Цена перерисовки​

Список из двухсот операций. Изменилось одно число. Есть два способа обновить страницу, и прогон меряет оба:

── список из 200 строк ──
узлов в таблице: 400
полная перерисовка : 33.27 мс
изменение одной ячейки : 0.003 мс
разница : в 9981 раз
ТАБЛИЦА НА 200 СТРОК: ИЗМЕНИЛОСЬ ОДНО ЧИСЛОперерисовали всё целиком33.27 мспоменяли одну ячейку0.003 мс — в десять тысяч раз быстрееи дело не только в скорости — вот что не пережило перерисовку:поле внутри списка: фокус true → false, текст «моя заметка» → пустоподсветка строки, поставленная кодом: пропаладелать точечно — быстро и безопасно, но вручную это десятки мест, которые надо не забыть обновить
Десять тысяч раз разницы — и это ещё не главная беда полной перерисовки

Тридцать три миллисекунды — это два кадра. Если такое происходит на каждое нажатие клавиши в поле поиска, интерфейс ощутимо тормозит.

Что теряется при перерисовке​

Скорость — не главная проблема. Прогон ставит поле ввода внутрь перерисовываемого списка, набирает в нём текст и перерисовывает:

── поле ввода ВНУТРИ перерисовываемого списка ──
до перерисовки : фокус true, текст "моя заметка", курсор 3
после : фокус false, текст ""
это тот же узел? false

Пользователь набирал заметку. В этот момент обновилось число где-то в другой строке — и текст исчез, а курсор ушёл из поля.

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

Значит, делать надо точечно: менять ровно то, что изменилось. Это быстро и ничего не ломает — но писать вручную приходится десятки мест, и каждое надо не забыть обновить при любом изменении данных.

Отсюда и появились библиотеки для интерфейсов. Идея у них одна: вы описываете, как должен выглядеть интерфейс при таких данных, а сравнением старого с новым и точечными изменениями занимается библиотека. Следующая глава — про одну из них.

Типичные ошибки​

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

Поиск вернул пустоту, а к ней обратились. Знакомое по главе про пустоту сообщение: нельзя прочитать свойство у ничего. Первым делом — опечатка в селекторе.

Вставка строки от пользователя как разметки. Разобрано выше. Это не теоретическая опасность.

Забыли отменить поведение формы по умолчанию. Страница перезагружается, введённое теряется.

Обработчик на кнопке вместо события отправки формы. Клавиша ввода в поле перестаёт работать.

Обработчик вешают в цикле на каждый элемент списка. Работает, пока список не обновился: после перерисовки обработчики исчезли вместе со старыми узлами.

class вместо className при задании класса из кода. Слово занято языком.

Отвечено вопросов: 0 из 2
Что теряется при полной перерисовке списка?
Почему обработчики, навешенные в цикле на каждую строку списка, перестают работать?
Отвечено верно: 0 из 2

Слова главы​

  • Дерево документа — объекты в памяти, соответствующие элементам страницы; меняем объект — меняется экран.
  • Поиск по селектору — одиночный и множественный; второй возвращает список узлов, а не массив.
  • Текст против разметки — второе разбирает строку и выполняет то, что в ней; для чужих строк только первое.
  • Создание элемента — создать, наполнить, вставить; до вставки его на странице нет.
  • Атрибут данных — свой атрибут вида «данные-что-то»; так привязывают идентификатор к элементу.
  • Обработчик — подписка на событие; их может быть много на одном элементе.
  • Всплытие — событие поднимается по дереву, и по дороге срабатывают все обработчики.
  • Делегирование — один обработчик на контейнере вместо множества на элементах.
  • Отмена поведения по умолчанию — без неё форма перезагружает страницу, а ссылка уводит с неё.

Тренажёр: набрать вслепую​

Цель: скорость ≥ 100 зн/мин, точность ≥ 88%

const el = document.querySelector("#total");

На чемпионате​

Делегирование с самого начала. Список операций будет обновляться, и обработчики на каждой строке гарантированно отвалятся после первого же обновления.

Отмена поведения формы — в первой строке обработчика. До всего остального, чтобы не забыть.

Ничего из сети не вставлять разметкой. Адрес, имя токена, подпись операции — всё это приходит из контракта, и записать туда можно что угодно.

Кнопка на время операции выключается. Транзакция идёт несколько секунд; без блокировки пользователь нажмёт трижды и отправит три транзакции. Одна строка, а выглядит как забота.

И про подключение скрипта: <script src="app.js" defer> в служебной части. Тогда файл начинает грузиться сразу, а выполняется, когда дерево уже построено, — и поиск элементов работает.

Куда углубиться​

  • Наблюдатель за пересечением — узнать, что элемент появился в поле зрения; так делают подгрузку списка.
  • Хранилище браузера — сохранить выбранную сеть или тему между заходами.
  • Свои события — способ связать части интерфейса, не связывая их напрямую.
  • Отмена подписки — когда обработчики надо снимать и чем это грозит, если не снять.
  • Панель разработчика — просмотр дерева, обработчиков и того, что тормозит.

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

ВидеоJavaScript в браузере — DOM (Document Object Model) и BOM (Browser Object Model)Александр Ламков — Friendly Frontend
ВидеоУроки Javascript #5. Как Работать с DOM в JS? (+ 26 упражнений для полной прокачки).Василий Муравьев
ВидеоСобытия JavaScript: делегирование, поведение браузера по умолчанию, генерация собственных событийАлександр Ламков — Friendly Frontend
ВидеоJS Браузерные события: всплытие и погружение, способы обработки событий, отмена всплытияАлександр Ламков — Friendly Frontend
Видео#6 Фазы события, Всплытие и Делегирование на JavaScriptITDoctor

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.