Junior: питання на співбесіді з теми «DOM і події»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
element.onclick = fn - лише один обробник: наступне присвоєння перезаписує попередній. addEventListener додає скільки завгодно обробників на одну подію й має налаштування.
button.onclick = () => track('click');
button.onclick = () => openModal(); // track більше не спрацює
button.addEventListener('click', () => track('click'));
button.addEventListener('click', () => openModal()); // спрацюють обидва
Опції addEventListener:
once: true- обробник автоматично знімається після першого спрацювання.passive: true- обіцянка не викликатиpreventDefault(). Дляtouchstart/wheelце дозволяє браузеру прокручувати сторінку плавно, не чекаючи обробника.capture: true- обробник спрацює на фазі перехоплення, ще до цільового елемента.signal- зняти обробник черезAbortController, зручно для кількох обробників одразу.
Зняти обробник можна лише тим самим посиланням на функцію:
function onResize() { /* ... */ }
window.addEventListener('resize', onResize);
window.removeEventListener('resize', onResize); // працює
window.addEventListener('resize', () => {});
window.removeEventListener('resize', () => {}); // нічого не зніме: інша функція
Атрибути в HTML (<button onclick="...">) - найгірший варіант: код змішано з розміткою, а суворий Content Security Policy такі обробники блокує.
Події:
DOMContentLoaded- HTML повністю розібрано, DOM готовий. Зображення, стилі (крім тих, що блокують скрипти) й iframe ще можуть вантажитися. Тут зазвичай ініціалізують скрипти.load(наwindow) - завантажено все: зображення, шрифти, iframe. Відбувається пізніше, часто значно.
Звичайний <script src> у <head> зупиняє розбір HTML, доки скрипт не завантажиться й не виконається. Сторінка «біла», а скрипт ще й не бачить елементи нижче.
defer- завантажити паралельно з розбором, а виконати після розбору HTML, передDOMContentLoaded, у порядку появи в документі.async- завантажити паралельно й виконати, щойно завантажився, у довільному порядку, перериваючи розбір.
<script defer src="/app.js"></script> <!-- основний код, залежить від DOM -->
<script async src="/analytics.js"></script> <!-- незалежний, порядок не важливий -->
<script type="module" src="/main.js"></script> <!-- модулі поводяться як defer -->
Правило: код застосунку - defer (або type="module"), незалежні сторонні скрипти (аналітика, віджети) - async. Розміщувати скрипти в кінці <body> - старий спосіб досягти схожого ефекту; з defer у цьому немає потреби.
querySelector(selector) повертає перший елемент, що відповідає CSS-селектору, або null. querySelectorAll(selector) - усі відповідні елементи.
const form = document.querySelector('#order-form');
const firstError = document.querySelector('.field.is-invalid input');
const buttons = document.querySelectorAll('[data-action="delete"]');
buttons.forEach((button) => button.addEventListener('click', onDelete));
Селектор - будь-який валідний CSS: класи, атрибути, псевдокласи (:checked, :not(...)), комбінатори.
querySelectorAll повертає статичний NodeList - знімок на момент виклику. Елементи, додані пізніше, у ньому не з'являться. У NodeList є forEach, але немає map і filter - для них [...nodes] чи Array.from(nodes).
Старші методи:
getElementById('id')- найшвидший пошук за id, повертає елемент абоnull;getElementsByClassName,getElementsByTagName- повертають живуHTMLCollection, яка оновлюється разом з DOM. Це може дивувати: видалення елементів у циклі по такій колекції пропускає кожен другий.
Пошук не лише від document. Усі методи працюють і на елементі - пошук тоді обмежується його нащадками:
const card = document.querySelector('.product-card');
const price = card.querySelector('.price');
closest(selector) шукає вгору - найближчого предка (або сам елемент), що відповідає селектору. Незамінний при делегуванні подій:
document.addEventListener('click', (event) => {
const row = event.target.closest('tr[data-id]');
if (row) openOrder(row.dataset.id);
});
matches(selector) - чи відповідає сам елемент селектору.
Пастки:
- перевіряти на
null:document.querySelector('.missing').classListкинеTypeError. Або?., або явна перевірка; - id, що починаються з цифри чи містять спецсимволи, треба екранувати в селекторі:
document.querySelector(`#${CSS.escape(id)}`); - скрипт виконується до розмітки: якщо
<script>у<head>безdefer, елементів ще немає. Розв'язується атрибутомdeferчиtype="module"; - динамічний вміст (Livewire, Alpine, підвантаження) замінює елементи - збережені посилання вказують на вузли, яких уже немає на сторінці. Делегування подій чи повторний пошук після оновлення.
Це два незалежні механізми, які часто плутають.
preventDefault() скасовує стандартну дію браузера для події:
form.addEventListener('submit', async (event) => {
event.preventDefault(); // не перезавантажувати сторінку
await fetch(form.action, { method: 'POST', body: new FormData(form) });
});
link.addEventListener('click', (event) => {
event.preventDefault(); // не переходити за посиланням
openModal(link.href);
});
Типові стандартні дії: відправка форми, перехід за посиланням, встановлення галочки чекбокса, введення символу в поле при keydown, контекстне меню при правому кліку, прокрутка колесом.
stopPropagation() зупиняє поширення події DOM-деревом: батьківські елементи її не отримають.
dropdown.addEventListener('click', (event) => {
event.stopPropagation(); // клік усередині меню не дійде до document
});
document.addEventListener('click', () => closeAllDropdowns());
Стандартна дія при цьому не скасовується: посилання всередині все одно спрацює.
stopImmediatePropagation() - ще й не викликати інші обробники цього самого елемента, зареєстровані пізніше.
Чому stopPropagation варто уникати:
- він ламає код, що спирається на спливання: делегування подій, аналітику, закриття випадних меню кліком поза ними, обробники фреймворків на
document; - проблему зазвичай краще вирішити перевіркою в батьківському обробнику:
if (dropdown.contains(event.target)) return;.
return false в обробнику, призначеному через onclick="...", скасовує стандартну дію. В addEventListener повернене значення ігнорується - потрібен явний preventDefault().
Пасивні обробники ({ passive: true }) обіцяють браузеру, що preventDefault() не буде викликано. Тоді браузер може почати прокрутку, не чекаючи на обробник. Виклик preventDefault() у пасивному обробнику ігнорується (з попередженням у консолі).
Перевірка: event.defaultPrevented показує, чи хтось уже скасував стандартну дію - корисно, щоб не обробляти подію двічі.
У фреймворках: Alpine @submit.prevent, @click.stop; Livewire wire:submit сам скасовує стандартну відправку форми; Vue - ті самі модифікатори .prevent і .stop.