Skip to main content

Що таке DOM?

DOM (Document Object Model) - це живе дерево об'єктів, яке браузер будує з HTML-документа в пам'яті, дозволяючи JavaScript читати, змінювати, додавати і видаляти вузли без перезавантаження сторінки.

Теорія

TL;DR

  • DOM - це те, що браузер збудував з HTML і тримає в пам'яті як змінюване дерево об'єктів, а не статичний файл
  • Аналогія: HTML - роздрукований рецепт; DOM - жива кухня, де шеф-кухар може поміняти або додати інгредієнти прямо під час готування
  • Головна різниця: HTML парситься один раз і лежить на диску; DOM живе в пам'яті й постійно змінюється
  • Прямі DOM API - для дрібних скриптів і розширень; React або Preact - коли оновлення стають частими або складними

Швидкий приклад

html
<!DOCTYPE html> <html> <body> <p id="demo">Оригінальний текст</p> <script> const para = document.getElementById('demo'); para.textContent = 'DOM змінив це!'; // Без перезавантаження console.log(para.nodeName); // "P" </script> </body> </html>

getElementById повертає живий об'єкт вузла. Зміна textContent одразу запускає перемальовування в рендерному пайплайні браузера. Це і є весь цикл.

HTML проти DOM

HTML - статичний файл розмітки. Браузер парсить його один раз і будує дерево DOM в пам'яті. Після цього вихідний файл більше не потрібен: те, що бачить користувач, - це DOM. Тому JavaScript може змінювати сторінку, не торкаючись вихідного файлу.

DOM дає стандартизовані об'єкти вузлів: ElementNode, TextNode, CommentNode. У кожного є методи на кшталт appendChild(). Коли ти їх викликаєш, браузер автоматично ставить оновлення екрана в чергу.

Типи вузлів

Кожен елемент у дереві DOM - це вузол (node). Чотири типи трапляються найчастіше:

  • Element node (<div>, <p> тощо) - nodeType: 1
  • Text node (текстовий вміст всередині тегів) - nodeType: 3
  • Comment node (HTML-коментарі) - nodeType: 8
  • Document node (корінь дерева, document) - nodeType: 9

Це важливо при переборі. childNodes повертає всі типи, включно з текстовими вузлами від пробілів між тегами. children повертає тільки вузли-елементи. Переплутаєш - отримаєш Cannot set properties of undefined на вузлі з пробілом.

Коли використовувати

  • Валідація форми або перемикання класу - прямі DOM API підходять
  • Браузерні розширення або буклети - фреймворк просто недоступний
  • Швидке прототипування - без налаштування збірки
  • Часті або масові оновлення, наприклад рендер 1000 елементів списку - перейди на бібліотеку з virtual DOM, як React; її reconciler батчить зміни і мінімізує reflow

Що відбувається в браузері при мутації DOM

Движок Blink у Chrome зберігає DOM як C++ об'єкти Node, доступні для JavaScript через WebIDL-прив'язки. Виклик querySelector() змушує V8 обходити дерево в глибину (O(n) у гіршому випадку). Мутація на кшталт appendChild() позначає відповідну гілку як брудну і ставить у чергу перерахунок стилів, layout (reflow), paint і compositing. Якщо групувати мутації всередині requestAnimationFrame, всі ці проходи зливаються в один - так отримують 60fps.

Node.js не має DOM. Для тестування парсингу HTML у Node використовується jsdom.

Типові помилки

Помилка 1: innerHTML з даними користувача

javascript
// Неправильно: парсить і виконує розмітку element.innerHTML = userInput; // <script>alert('XSS')</script> запускається // Правильно: все трактується як звичайний текст element.textContent = userInput;

На код-рев'ю я бачу innerHTML = userInput набагато частіше, ніж хотілося б. Зазвичай це дедлайн-код, де людина просто хотіла щоб спрацювало, не подумавши про джерело даних. Для будь-яких даних від користувача використовуй textContent або createTextNode().

Помилка 2: Не перевіряєш null

javascript
// Неправильно const el = document.getElementById('nonexistent'); // null el.textContent = 'hi'; // TypeError: Cannot set property // Правильно if (el) el.textContent = 'hi';

getElementById повертає null, якщо нічого не знайдено. Один непровірений результат ламає весь скрипт.

Помилка 3: Перебір живої колекції з мутацією

javascript
// Неправильно: getElementsByClassName повертає живу HTMLCollection const items = document.getElementsByClassName('old-item'); Array.prototype.forEach.call(items, item => item.remove()); // пропускає кожен другий // Правильно: спочатку перетвори на статичний масив Array.from(document.getElementsByClassName('old-item')).forEach( item => item.remove() );

getElementsByClassName і getElementsByTagName повертають живі колекції. Видалення елемента зміщує індекси, і цикл пропускає наступний сусід щоразу. querySelectorAll повертає статичний знімок і цієї проблеми не має.

Помилка 4: children проти childNodes

javascript
// Працює: children містить тільки Element-вузли parent.children[0].style.color = 'red'; // Може впасти: childNodes[0] може бути TextNode з пробілом parent.childNodes[0].style.color = 'red'; // Cannot set properties of undefined

Помилка 5: Не видаляєш обробники перед видаленням вузла

javascript
const btn = document.createElement('button'); btn.addEventListener('click', handler); document.body.appendChild(btn); btn.remove(); // обробник залишається в пам'яті // Правильно btn.removeEventListener('click', handler); btn.remove();

У single-page app, де елементи постійно створюються і видаляються, осиротілі обробники накопичуються у реальний витік пам'яті.

Де зустрічається на практиці

  • React: ReactDOM.render() перетворює JSX на реальні DOM-мутації через createElement і appendChild
  • jQuery (легасі): $(el).find() обгортає querySelectorAll з крос-браузерною сумісністю
  • Puppeteer: page.$eval() виконує JavaScript на живому DOM для end-to-end тестів
  • WordPress / SSR-додатки: сервер рендерить HTML, потім клієнтський JavaScript гідратує DOM для інтерактивності

Можливі питання на співбесіді

Q: Яка різниця між textContent і innerHTML?
A: textContent встановлює звичайний текст і екранує все. innerHTML парсить рядок як розмітку і може виконати скрипти. Для будь-яких даних від користувача завжди використовуй textContent.

Q: Як querySelector обходить дерево?
A: Обхід у глибину, pre-order, починаючи від кореня документа. Зупиняється на першому збігу. Тому він швидший за querySelectorAll, якщо потрібен один елемент.

Q: Яка різниця між реальним DOM і virtual DOM?
A: Реальний DOM - фактичне дерево браузера. Virtual DOM - звичайний JavaScript-об'єкт, що його відображає. React порівнює старе і нове віртуальне дерево, потім застосовує мінімальний набір змін до реального DOM.

Q: Які є типи вузлів у DOM?
A: Основні: Element (nodeType 1), Text (nodeType 3), Comment (nodeType 8), Document (nodeType 9). Перевіряєш через node.nodeType.

Q (рівень senior): Що відбувається в Blink, коли викликаєш appendChild()?
A: Вузол вставляється в C++ дерево. Це позначає відповідну гілку як брудну і планує прохід перерахунку стилів, потім layout (reflow для геометрії), paint (малювання пікселів) і compositing. Якщо групувати мутації до наступного кадру через requestAnimationFrame, всі ці проходи зливаються в один - так отримуєш 60fps у DOM-важкому коді.

Приклади

Базовий: зміна текстового вмісту

html
<!DOCTYPE html> <html> <body> <p id="demo">Оригінальний текст</p> <script> const para = document.getElementById('demo'); para.textContent = 'Оновлено через JS'; // запускає перемальовування console.log(para.nodeName); // "P" </script> </body> </html>

Знайти вузол за ID і оновити його текст - найпоширеніша операція з DOM. Без перезавантаження, без фреймворку.

Середній: живий список задач

html
<!DOCTYPE html> <html> <body> <ul id="todos"></ul> <input id="task" placeholder="Назва задачі" /> <button id="add">Додати</button> <script> const ul = document.getElementById('todos'); const input = document.getElementById('task'); document.getElementById('add').addEventListener('click', () => { const li = document.createElement('li'); li.textContent = input.value; // textContent захищає від XSS ul.appendChild(li); input.value = ''; }); </script> </body> </html>

Це те, що робить TodoMVC у своїй vanilla JS реалізації. createElement будує вузол, appendChild вставляє його, textContent тримає все безпечним. React зі своїм useState і JSX робить те саме під капотом, тільки з кроком diff перед тим як торкнутися реального DOM.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?