Как перестать бояться DOM, использовать его в полной мере и действительно начать любить его
Когда я впервые начал работать профессиональным веб-разработчиком в 2008 году, я немного знал HTML, CSS и PHP. В то же время я также изучал эту вещь, называемую JavaScript, потому что она позволяла мне показывать и скрывать элементы и делать такие классные вещи, как выпадающие меню.
В то время я работал в небольшой компании, которая в основном создавала системы CMS для клиентов, и нам требовался загрузчик нескольких файлов. То, что в то время было невозможно с родным JavaScript.
После непродолжительных поисков я нашел навороченное решение на основе Flash и эту библиотеку JavaScript под названием MooTools. В 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(/* your selector */)
Здесь вы можете использовать любой селектор:
document.querySelector('.foo') // class selector document.querySelector('#foo') // id selector document.querySelector('div') // tag selector document.querySelector('[name="foo"]') // attribute selector document.querySelector('div + p > span') // you go girl!
Если не найдено ни одного элемента, он вернет null
.
Несколько элементов
Чтобы выбрать несколько элементов, используйте:
document.querySelectorAll('p') // selects all <p> elements
Вы можете использовать document.querySelectorAll
так же, как document.querySelector
. Подойдет любой допустимый селектор CSS, с той лишь разницей, что querySelector
вернет один элемент, тогда как querySelectorAll
вернет статический NodeList
, содержащий найденные элементы. Если элементы не найдены, возвращается пустой NodeList
.
NodeList
- это повторяемый объект, который подобен массиву, но не в действительности массив, поэтому у него разные методы. На нем можно запустить forEach
, но нельзя, например, map
, reduce
или find
.
Если вам действительно нужно запустить на нем методы массива, вы можете просто превратить его в массив с помощью деструктуризации или Array.from
:
const arr = [...document.querySelectorAll('p')]; or const arr = Array.from(document.querySelectorAll('p')); arr.find(element => {...}); // .find() now works
Метод 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') // finds all <p> tags in #container only
Но все равно многословно!
Если вас все еще беспокоят лишние нажатия клавиш, вы можете использовать оба метода:
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');
Будет найден ближайший родительский <div>
элемент абзаца, выбранного document.querySelector('p')
. Вы можете связать эти вызовы, чтобы продвинуться дальше по дереву:
document.querySelector('p').closest('div').closest('.content');
Добавление элементов
Код для добавления одного или нескольких элементов в дерево DOM известен своей быстрой подробностью. Допустим, вы хотите добавить на свою страницу следующую ссылку:
<a href="/home" class="active">Home</a>
Вам нужно будет сделать:
const link = document.createElement('a'); link.setAttribute('href', '/home'); link.className = 'active'; link.textContent = 'Home'; document.body.appendChild(link);
А теперь представьте, что вам нужно сделать это для 10 элементов…
По крайней мере, jQuery позволяет:
$('body').append('<a href="/home" class="active">Home</a>');
Ну что ж? Есть нативный эквивалент:
document.body.insertAdjacentHTML('beforeend', '<a href="/home" class="active">Home</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>Title</h1> </div> <div class="second"> <h2>Subtitle</h2> </div>
а <h2>
вставляется после <h1>
:
const h1 = document.querySelector('h1'); const h2 = document.querySelector('h2'); h1.insertAdjacentElement('afterend', h2);
он будет просто перемещен, не скопирован:
<div class="first"> <h1>Title</h1> <h2>Subtitle</h2> </div> <div class="second"> </div>
Замена элементов
Элемент DOM можно заменить любым другим элементом DOM, используя его метод replaceWith
:
someElement.replaceWith(otherElement);
Элемент, которым он заменяется, может быть новым элементом, созданным с помощью document.createElement
, или элементом, который уже является частью того же документа (в этом случае он снова будет перемещен, а не скопирован):
<div class="first"> <h1>Title</h1> </div> <div class="second"> <h2>Subtitle</h2> </div> const h1 = document.querySelector('h1'); const h2 = document.querySelector('h2'); h1.replaceWith(h2); // result: <div class="first"> <h2>Subtitle</h2> </div> <div class="second"> </div>
Удаление элементов
Просто вызовите его remove
метод:
const container = document.querySelector('#container'); container.remove(); // hasta la vista, baby
Намного лучше, чем по старинке:
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">Home</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, does not have class "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 is contained by container and follows container container.compareDocumentPosition(h1); // 10: container contains h1 and precedes it h1.compareDocumentPosition(container); // 4: h2 follows h1 h1.compareDocumentPosition(h2); // 2: h1 precedes 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
nodes, а неHTMLElement
s. Чтобы это работало, наблюдаемый узел должен бытьText
узлом или, если наблюдатель отслеживаетHTMLElement
, параметрsubtree
должен быть установлен наtrue
, чтобы также отслеживать изменения в дочерних узлах.characterDataOldValue
: при установке наtrue
предыдущее значение разделенных данных будет записываться всякий раз, когда происходит изменениеsubtree
: установите значениеtrue
, чтобы также наблюдать изменения в дочерних узлах наблюдаемого элемента.childList
: установите значениеtrue
для отслеживания элемента на предмет добавления и удаления дочерних узлов. Если дляsubtree
установлено значениеtrue
, дочерние элементы также будут отслеживаться на предмет добавления и удаления дочерних узлов.
Когда наблюдение за элементом началось с вызова observe
, обратный вызов, переданный конструктору MutationObserver
, вызывается с массивом объектов MutationRecord
, описывающих произошедшие изменения и вызываемого наблюдателя в качестве второго параметра.
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': // the name of the changed attribute is in // mutation.attributeName // and its old value is in mutation.oldValue // the current value can be retrieved with // target.getAttribute(mutation.attributeName) break; case 'childList': // any added nodes are in mutation.addedNodes // any removed nodes are in mutation.removedNodes break; } }); }; const observer = new MutationObserver(callback); observer.observe(target, { attributes: true, attributeFilter: ['foo'], // only observe attribute 'foo' attributeOldValue: true, childList: true });
Когда вы закончите наблюдение за целью, вы можете отключить наблюдателя и, если необходимо, вызвать его метод takeRecords
для получения любых ожидающих мутаций, которые еще не были доставлены в обратный вызов:
const mutations = observer.takeRecords(); callback(mutations); observer.disconnect();
Не бойтесь DOM
DOM API - это невероятно мощный и универсальный, хотя и подробный API. Имейте в виду, что он предназначен для предоставления разработчикам низкоуровневых строительных блоков для построения абстракций, поэтому в этом смысле он должен быть подробным, чтобы предоставить однозначный и понятный API.
Дополнительные нажатия клавиш не должны отпугивать вас от использования его в полной мере.
DOM - это важное знание для каждого разработчика JavaScript, поскольку вы, вероятно, используете его каждый день. Не бойтесь этого и используйте его в полной мере.
Вы станете лучшим разработчиком для этого.