Когда я начал карьеру профессионального веб-разработчика в 2008 году, то знал понемногу HTML, CSS и PHP. А также изучал JavaScript, потому что с помощью него я показывал и скрывал элементы, делал классные вещи, такие как выпадающие меню.
Тогда я работал в небольшой компании, которая создавала системы CMS для клиентов, и потребовался загрузчик нескольких файлов. То, что невозможно сделать в то время с нативным JavaScript.
После поисков я нашёл забавное решение на основе Flash и библиотеки JavaScript под названием MooTools. Там есть классная функция $
для выбора элементов DOM и модули, такие как индикаторы выполнения и запросы Ajax. Две недели спустя я открыл для себя jQuery и поразился.
Нет многословных, неуклюжих манипуляций с DOM, только простые связанные селекторы и масса полезных плагинов.
Перенесёмся в 2019 год, где миром правят фреймворки. Если ты начал путь веб-разработчика в последнее десятилетие, велика вероятность, что в будущем не столкнёшься с «сырым» DOM. Возможно, тебе это и не нужно.
Хотя такие фреймворки, как Angular и React, привели к стремительному спаду популярности jQuery, он по-прежнему используется ошеломляющими 66 миллионами веб-сайтов. По оценкам это составляет 74% всех сайтов в мире.
Наследие jQuery впечатляет, и показательный пример его влияния на стандарты – методы querySelector
и querySelectorAll
, которые имитируют функцию jQuery $
.
По иронии судьбы эти два метода стали главной причиной снижения популярности jQuery, поскольку заменили наиболее используемую функциональность jQuery: простой выбор элементов DOM.
Но чистый DOM API многословен.
Я имею в виду $
в сравнении с document.querySelectorAll
.
И это то, что мешает разработчикам использовать нативный DOM API. Но здесь на самом деле нет необходимости.
Нативный DOM API великолепен и чрезвычайно полезен. Да, многословен, но это потому, что предоставляет низкоуровневые блоки для построения абстракций. И если переживаешь насчёт дополнительных нажатий клавиш, расслабься: у всех современных редакторов и IDE превосходное автозавершение кода. Ещё вариант: создавай псевдонимы наиболее часто используемых функций.
Поехали!
Выбор элементов
Один элемент
Чтобы выбрать один элемент с использованием любого допустимого селектора CSS, напиши:
document.querySelector(/* твой селектор */
Подставляй в код любой селектор:
document.querySelector('.foo') // селектор класса
document.querySelector('#foo') // селектор идентификатора
document.querySelector('div') // селектор тега
document.querySelector('[name="foo"]') // селектор атрибута
document.querySelector('div + p > span') // так держать!
Когда нет соответствующих элементов, возвращается null
.
Несколько элементов
Для выбора нескольких элементов используй:
document.querySelectorAll('p') // выбирает все элементы <p>
Применяй document.querySelectorAll
, как и document.querySelector
. Подойдёт любой действительный селектор CSS, и единственное отличие: querySelector
вернёт один элемент, а querySelectorAll
– статический NodeList
с найденными элементами. Если не найдено ни одного элемента, получишь пустой NodeList
.
A NodeList
– итерируемый объект, похожий на массив, но не массив, поэтому у него нет тех же методов. Ты запустишь forEach
на нём, но не map
, reduce
или find
.
Если для этого объекта понадобились методы массива, то преобразуй его в массив с помощью деструктуризации или Array.from
:
const arr = [...document.querySelectorAll('p')];
or
const arr = Array.from(document.querySelectorAll('p'));
arr.find(element => {...}); // .find () теперь работает
querySelectorAll
отличается от методов, наподобие getElementsByTagName
и getElementsByClassName
тем, что они возвращают динамическую коллекцию HTMLCollection
, а querySelectorAll
– статический NodeList
.
И если сделать getElementsByTagName('p')
и убрать один <p>
из документа, он удаляется также из полученной HTMLCollection
.
Но если напишешь querySelectorAll('p')
и удалишь один <p>
из документа, он останется в полученномNodeList
.
Другое отличие состоит в том, что HTMLCollection
содержит элементы HTMLElement
, а NodeList
– любой тип Node
.
Относительные поиски
Нет необходимости вызывать querySelector(All)
с document
. Запускай относительный поиск на любом HTMLElement
:
const div = document.querySelector('#container');
div.querySelectorAll('p') // находит все теги <p> только в #container
Но это по-прежнему многословно!
Если ты всё ещё беспокоишься о дополнительных нажатиях клавиш, применяй оба:
const $ = document.querySelector.bind(document);
$('#container');
const $$ = document.querySelectorAll.bind(document);
$$('p');
Поднимаемся по дереву DOM
Использование CSS-селекторов для выбора DOM-элементов означает, что мы спускаемся по дереву DOM. Нет CSS-селекторов для движения вверх и выбора родителей.
Но перемещаться вверх по дереву DOM можно с помощью closest()
, который также принимает любой валидный CSS-селектор:
document.querySelector('p').closest('div');
Это найдёт ближайший родительский элемент абзаца, выбранного выражением document.querySelector('p')
. Свяжи эти вызовы, чтобы подняться выше по дереву:
document.querySelector('p').closest('div').closest('.content');
Добавление элементов
Код для добавления одного или нескольких элементов в дерево DOM пользуется дурной славой за счёт быстрого нагромождения словарных конструкций. Чтобы добавить следующую ссылку на страницу:
<a href="/home" class="active">Главная страница</a>
Тебе понадобится:
const link = document.createElement('a');
a.setAttribute('href', '/home');
a.className = 'active';
a.textContent = 'Главная страница';
document.body.appendChild(link);
Представь, что придётся сделать это для 10 элементов...
А в jQuery получится так:
$('body').append('<a href="/home" class="active">Главная страница</a>');
А знаешь что? Есть нативный эквивалент:
document.body.insertAdjacentHTML('beforeend',
'<a href="/home" class="active">Главная страница</a>');
Метод insertAdjacentHTML
вставляет произвольную действительную HTML-строку в DOM на четыре позиции, указанные в первом параметре:
'beforebegin'
: перед элементом.'afterbegin'
: внутри элемента перед его первым потомком.'beforeend'
: внутри элемента после его последнего потомка.-
'afterend'
: после элемента .
<!-- beforebegin -->
<p>
<!-- afterbegin -->
foo
<!-- beforeend -->
</p>
<!-- afterend -->
Это также упрощает указание точного места для вставки нового элемента. Допустим, нужен <a>
прямо перед этим <p>
. Без insertAdjacentHTML
сделали бы:
const link = document.createElement('a');
const p = document.querySelector('p');
p.parentNode.insertBefore(link, p);
Теперь хватит этого:
const p = document.querySelector('p');
p.insertAdjacentHTML('beforebegin', '<a></a>');
Здесь также не обошлось без эквивалентного метода для вставки элементов DOM:
const link = document.createElement('a');
const p = document.querySelector('p');
p.insertAdjacentElement('beforebegin', link);
и текста:
p.insertAdjacentText('afterbegin', 'foo');
Двигаем элементы
Метод insertAdjacentElement
также используется для перетасовки существующих элементов в том же документе. Когда элемент, который вставляем с помощью insertAdjacentElement
, уже присутствует в документе, он просто перемещается.
Когда такой HTML:
<div class="first">
<h1>Заголовок</h1>
</div>
<div class="second">
<h2>Подзаголовок</h2>
</div>
и <h2>
вставляется после <h1>
:
const h1 = document.querySelector('h1');
const h2 = document.querySelector('h2');
h1.insertAdjacentElement('afterend', h2);
он просто сдвигается, а не копируется:
<div class="first">
<h1>Заголовок</h1>
<h2>Подзаголовок</h2>
</div>
<div class="second">
</div>
Заменяем элементы
Элемент DOM заменяется любым другим элементом DOM с использованием метода replaceWith
:
someElement.replaceWith(otherElement);
Заменой выступает новый элемент, созданный с помощью document.createElement
, или элемент, который уже есть в том же документе (тогда он снова будет перемещён, а не скопирован):
<div class="first">
<h1>Заголовок</h1>
</div>
<div class="second">
<h2>Подзаголовок</h2>
</div>
const h1 = document.querySelector('h1');
const h2 = document.querySelector('h2');
h1.replaceWith(h2);
// результат:
<div class="first">
<h2>Подзаголовок</h2>
</div>
<div class="second">
</div>
Удаляем элементы
Просто вызывай у элемента метод remove
:
const container = document.querySelector('#container');
container.remove(); // до свидания, детка
Лучше, чем по-старому:
const container = document.querySelector('#container');
container.parentNode.removeChild(container);
Создаём элемент из сырого HTML
insertAdjacentHTML
вставляет сырой HTML в документ, но что если хотим создать элемент из сырого HTML и использовать его позже?
Для этого понадобится объект DomParser
и метод parseFromString
. DomParser
преобразует исходный код HTML или XML в документ DOM. Используем метод parseFromString
для создания документа с одним элементом и возвращаем только этот элемент:
const createElement = domString => new
DOMParser().parseFromString(domString, 'text/html').body.firstChild;
const a = createElement('<a href="/home" class="active">Главная страница</a>');
Инспектируем DOM
Стандартный DOM API также предоставляет методы для инспекции DOM. Например, matches
проверяет соответствие элемента определённому селектору:
<p class="foo">Hello world</p>
const p = document.querySelector('p');
p.matches('p'); // true
p.matches('.foo'); // true
p.matches('.bar'); // false, нет класса "bar"
А также проверим, будет ли элемент дочерним по отношению к другому элементу с помощью contains
:
<div class="container">
<h1 class="title">Foo</h1>
</div>
<h2 class="subtitle">Bar</h2>
const container = document.querySelector('.container');
const h1 = document.querySelector('h1');
const h2 = document.querySelector('h2');
container.contains(h1); // true
container.contains(h2); // false
Чтобы получить больше информации об элементах, используй compareDocumentPosition
. Этот метод определяет, предшествует ли один элемент другому элементу или следует за ним, или же один из них содержит другой. Возвращает целое число, которое представляет собой отношение между сравниваемыми элементами.
Вот пример с элементами из предыдущего фрагмента:
<div class="container">
<h1 class="title">Foo</h1>
</div>
<h2 class="subtitle">Bar</h2>
const container = document.querySelector('.container');
const h1 = document.querySelector('h1');
const h2 = document.querySelector('h2');
// 20: h1 содержится в элементе container и следует за container
container.compareDocumentPosition(h1);
// 10: 10: container содержит h1 и предшествует ему
h1.compareDocumentPosition(container);
// 4: h2 следует за h1
h1.compareDocumentPosition(h2);
// 2: h1 предшествует h2
h2.compareDocumentPosition(h1);
Возвращаемое значение compareDocumentPosition
– целое число, биты которого представляют собой отношение между узлами касательно аргумента, указанного в этом методе.
Таким образом, с учётом синтаксиса node.compareDocumentPostion(otherNode)
возвращаемое число означает:
- 1: узлы не части одного и того же документа.
- 2:
otherNode
предшествуетnode
- 4:
otherNode
следует заnode
. - 8:
otherNode
содержитnode
. - 16:
otherNode
содержится вnode
.
Поскольку устанавливается не один бит, в приведённом выше примере container.compareDocumenPosition(h1)
возвращает 20, когда ожидалось 16, ведь h1
содержится в container
. Но h1
также следует за элементом container
(4), поэтому полученное значение равно 16 + 4 = 20.
Больше подробностей, пожалуйста!
Чтобы отслеживать изменения в любом узле DOM, используй интерфейс MutationObserver
. Это включает в себя изменения текста, добавление или удаление узлов из наблюдаемого элемента или изменение атрибутов узла.
MutationObserver
– невероятно мощный API для наблюдения практически за любыми изменениями, которые происходят в элементе DOM и его дочерних узлах.
Новый MutationObserver
создаётся путём вызова конструктора с функцией обратного вызова. Этот обратный вызов будет запускаться всякий раз, когда изменяется наблюдаемый узел:
const observer = new MutationObserver(callback);
Чтобы отслеживать элемент, вызываем метод наблюдателя observe
, где исследуемый узел будет первым аргументом и объект с параметрами – вторым.
const target = document.querySelector('#container');
const observer = new MutationObserver(callback);
observer.observe(target, options);
Наблюдение начинается после вызова observe
. Объект параметров принимает следующие ключи:
attributes
: когда значениеtrue
, изменения атрибутов узла будут отслеживаться.attributeFilter
: массив имён атрибутов для мониторинга, когдаattributes
равноtrue
, а этот ключ не задан, наблюдаются изменения всех атрибутов узла.attributeOldValue
: при установкеtrue
записывается предыдущее значение атрибута при каждом изменении.characterData
: когда значениеtrue
, регистрирует изменения текста текстового узла, так что подходит для элементовText
, а неHTMLElement
. Чтобы это работало, узел должен быть объектомText
или, если наблюдатель отслеживаетHTMLElement
, требуется значениеtrue
у параметраsubtree
для мониторинга изменений дочерних узлов.characterDataOldValue
: еслиtrue
, регистрируется предыдущее значение символьных данных при каждом изменении.subtree
: когдаtrue
, также отслеживаются изменения дочерних узлов наблюдаемого элемента.childList
: установиtrue
, чтобы контролировать добавление и удаление дочерних узлов элемента. Еслиsubtree
задано значениеtrue
, для дочерних элементов также отслеживаются удаление и добавление дочерних узлов.
Когда начинаешь мониторинг элемента при запуске observe
, обратный вызов в конструкторе MutationObserver
вызывается с массивом объектов MutationRecord
, описывающих произошедшие изменения, и наблюдателем в качестве второго параметра.
A MutationRecord
содержит следующие свойства:
type
: тип изменения,attributes
,characterData
либоchildList
.target
: изменённый элемент: его атрибуты, символьные данные или дочерние элементы.addedNodes
: список добавленных узлов или пустойNodeList
, если ничего не добавлялось.removedNodes
: список удалённых узлов или пустойNodeList
, если ничего не удалялось.attributeName
: имя изменённого атрибута илиnull
, если атрибуты не изменялись.previousSibling
: предыдущий смежный элемент добавленных или удалённых узлов илиnull
.nextSibling
: следующий смежный элемент добавленных или удалённых узлов илиnull
.
Допустим, будем наблюдать изменения в атрибутах и дочерних узлах:
const target = document.querySelector('#container');
const callback = (mutations, observer) => {
mutations.forEach(mutation => {
switch (mutation.type) {
case 'attributes':
// имя изменённого атрибута находится в
// mutation.attributeName
// и его старое значение содержится в mutation.oldValue
// текущее значение получаем с помощью
// target.getAttribute(mutation.attributeName)
break;
case 'childList':
// добавленные узлы хранятся в mutation.addedNodes
// удалённые узлы – в mutation.removedNodes
break;
}
});
};
const observer = new MutationObserver(callback);
observer.observe(target, {
attributes: true,
attributeFilter: ['foo'], // отслеживает только атрибут 'foo'
attributeOldValue: true,
childList: true
});
Когда завершаем мониторинг, отключаем наблюдателя и при необходимости вызываем метод takeRecords
для получения незавершённых изменений, которые ещё не доставлены в обратный вызов:
const mutations = observer.takeRecords();
callback(mutations);
observer.disconnect();
Не бойся DOM
DOM API потрясает мощностью и универсальностью, хотя и многословен. Помни о его предназначении предоставлять разработчикам низкоуровневые строительные блоки для создания абстракций. В этом смысле он требует многословности, чтобы гарантировать однозначный и понятный API.
Пусть дополнительные нажатия клавиш не отпугивают от использования полного потенциала этого интерфейса.
DOM – необходимое знание для каждого JavaScript разработчика, поскольку, скорее всего, используешь его каждый день. Не бойся этого, используй в полной мере.
И тогда станешь лучшим разработчиком.
Комментарии