На практической части просят «без фреймворка» сделать список с удалением, табы, модалку, поиск с задержкой. Проверяют не знание редких методов, а несколько привычек. Разберём их на примерах.
1. Делегирование вместо сотни обработчиков
Не вешайте обработчик на каждый элемент списка. Один обработчик на контейнер, а нужный элемент ищется через closest.
list.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action="remove"]');
if (!btn || !list.contains(btn)) return;
btn.closest('li').remove();
});
Почему closest, а не e.target: клик мог прийти по иконке внутри кнопки, и e.target будет <svg>. Работает и для элементов, добавленных позже.
2. Данные в data-атрибутах
<li data-id="42">Купить хлеб <button data-action="remove">×</button></li>
const id = Number(li.dataset.id); // dataset всегда строки
3. Рендер списка без XSS и лишних перерисовок
function render(items) {
const frag = document.createDocumentFragment();
for (const item of items) {
const li = document.createElement('li');
li.dataset.id = item.id;
li.textContent = item.title; // не innerHTML с пользовательским текстом
frag.append(li);
}
list.replaceChildren(frag);
}
textContentвместоinnerHTMLдля пользовательских данных.DocumentFragmentили одинreplaceChildren, чтобы вставить всё за одну операцию.<template>удобен для сложной разметки:template.content.cloneNode(true).
4. Не смешивать чтение и запись раскладки
// плохо: на каждой итерации браузер пересчитывает layout
for (const el of items) el.style.width = container.offsetWidth + 'px';
// хорошо: прочитать один раз, потом писать
const w = container.offsetWidth;
for (const el of items) el.style.width = w + 'px';
5. Формы
form.addEventListener('submit', (e) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(form));
if (!data.title.trim()) return;
add(data);
form.reset();
});
Слушайте submit у формы, а не click у кнопки: так работает и Enter в поле. Для живого поиска событие input, а не keyup (вставка мышью и автозаполнение не дают keyup).
6. Debounce для поиска
function debounce(fn, ms) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), ms);
};
}
input.addEventListener('input', debounce((e) => search(e.target.value), 300));
Плюс отменять устаревший запрос через AbortController, чтобы старый ответ не перезаписал новый.
7. Модалка и клавиатура
- Встроенный
<dialog>сshowModal()даёт фон, закрытие по Esc, ловушку фокуса и инертность остальной страницы. - Закрытие по клику на фон:
dialog.addEventListener('click', (e) => { if (e.target === dialog) dialog.close(); }). - После закрытия фокус возвращается на кнопку, которая открыла модалку (для
<dialog>браузер делает это сам).
8. Убирать за собой
Обработчики на window и document, таймеры и наблюдатели снимайте, когда компонент удаляется. Удобно через AbortController:
const ac = new AbortController();
window.addEventListener('resize', onResize, { signal: ac.signal });
document.addEventListener('keydown', onKey, { signal: ac.signal });
// при уничтожении
ac.abort(); // снимет оба обработчика
Частые ошибки
e.targetвместоclosestв делегировании.innerHTML +=в цикле: каждый раз пересоздаёт всё содержимое и теряет обработчики и состояние полей.- Сравнивать
dataset.idс числом через===. - Слушать
clickпо кнопке отправки вместоsubmit.
Что спросят на собесе
- Сделай список задач с добавлением и удалением на чистом DOM.
- Почему делегирование и как оно работает с вложенными элементами.
- Как отрисовать тысячу элементов быстро.
- Как сделать поиск, который не шлёт запрос на каждую букву.
- Как сделать доступную модалку.