-
Notifications
You must be signed in to change notification settings - Fork 46
DOM
DOM (Document Object Model) - объектная модель, которая используется для XML/HTML-документов.
DOM - это внутреннее представление HTML-страницы в виде дерева, иерархии.
Каждый HTML-тег образует узел дерева с типом «элемент». Вложенные в него теги становятся дочерними узлами.
DOM is a representation — a model of a document and its content. — w3.org
DOM - It’s a way of representing a structured document via objects. — vuejsfeed.com
Во время загрузки браузер читает документ и тут же строит из него DOM, по мере получения информации достраивая новые узлы и тут же отображая их. Этот процесс идет непрерывным потоком.
Всего различают 12 типов узлов, но на практике используют 4:
- Документ – точка входа в DOM.
- Элементы – основные строительные блоки.
- Текстовые узлы – содержат текст.
- Комментарии – содержат информацию, которая не будет показана, но доступна из JS.
Пример комментария:
<!-- комментарий -->
DOM нужен для того, чтобы манипулировать страницей – читать информацию из HTML, создавать и изменять элементы.
Если попытаться получить элемент, которого нет или пока нет в DOM, получим null.
К примеру, в <head>
находится скрипт, в котором пытаемся получить доступ к <body>
.
Дочерние элементы (или дети) – элементы, которые лежат непосредственно внутри данного.
Потомки – все элементы, которые лежат внутри данного, вместе с их детьми, детьми их детей и так далее. То есть, всё поддерево DOM.
DOM-коллекции (псевдомассивы) не являются JS-массивами (не имеют forEach, map и друшгих методов).
childNodes - коллекция, содержащая все дочерние узлы (включая текстовые).
children - коллекция, содержащая только дочерние узлы-элементы (соответствуют тегам).
-
querySelector('css')
|querySelectorAll('css')
- по css-селектору -
getElementByClassName('classname')
|getElementsByClassName('classname')
- по классу -
getElementById('id')
|getElementsById('id')
- по id -
getElementByName('name')
|getElementsByName('name')
- по name -
getElementByTagName('tag')
|getElementsByTagName('tag')
по тэгам
Первая колонка возвращает первый элемент, вторая колонка возвращает список потомков.
'*' - получить всех потомков.
getElementById(id): Браузер поддерживает у себя внутреннее соответствие id -> элемент, поэтому нужный элемент возвращается сразу.
querySelector(query): Перебирает потомков, проверяет их на удовлетворение query, останавливается на первом.
querySelectorAll(query): Перебирает всех потомков, проверяет их на удовлетворение query, собирает из в коллекцию.
Переборы происходят довольно быстро, поскольку осуществляется движком браузера, а не JS кодом.
Для оптимизации последние результаты кешируются. Кэш хранится до тех пор, пока документ не изменятся.
Результат запросов getElementsBy* – это не массив, а специальный объект, имеющий тип NodeList или HTMLCollection. Он похож на массив, так как имеет нумерованные элементы и длину, но внутри это не готовая коллекция, а «живой поисковой запрос». Поиск выполняется при обращении к коллекции или её длине.
An HTMLCollection in the HTML DOM is live; it is automatically updated when the underlying document is changed. — developer.mozilla.org
Для оптимизации firefox запоминает все найденные элементы, webkit запоминает только последний. При попытке обратиться к одному из предыдущих элементов поиска firefox просто вернёт его, а webkit будет искать его с начала или с текущего элемента в зависимости от того, что ближе.
Таким образом, метод firefox требует большей памяти, но имеет большую эффективность.
При изменении документа кеш "умным образом" очищается (то есть очищается лишь то, что могло измениться в поиске).
elem.matches(css) - метод, проверяющий, удовлетворяет элемент css-селектору.
elem.closest(css) - метод, ищущий ближайший элемент выше по иерархии DOM (включая elem), удовлетворяющий css-селектору.
Разные узлы являются объектами разных классов, поэтому имеют разные свойства.
Примеры: <input>
– HTMLInputElement, <body>
– HTMLBodyElement, <a>
– HTMLAnchorElement
Различия console.log() и console.dir(): первый выводит элемент в удобном виде для исследования HTML-структуры, второй - в виде JS-объекта для анализа его свойств.
Узнать класс можно:
console.log(element.toString())
console.dir(element)
console.log(element.constructor)
console.log(element.__proto__)
Проверить класс можно:
console.log(element instanceof HTMLElement)
Cвойство nodeType содержит тип узла. Всего их 12. (element - 1, attribute - 2, text - 3, ... , comment - 8, ... ).
Свойство nodeName содержит название узла в uppercase.
Свойство tagName содержит тэг узла в uppercase. (только у element).
Свойство innerHTML хранит содержимое элемента в виде строки. Доступно для чтения и записи. Оно есть только у element. В него можно записать что угодно, но браузер исправит ошибки и вернёт валидный HTML-код. Оператор += для innerHTML не добавляет, а осуществляет перезапись всего содержимого, т.е. сперва всё удалится, а потом загрузится заново. Если в innerHTML есть скрипт с тэгом <script>, то он не выполнится.
Свойство outerHTML хранит содержимое элемента, включая сам элемент, в виде строки. Перезаписывать нельзя, потому что элемент не меняется, а заменяется на новый, который создаётся из нового outerHTML.
Свойство data хранит содержимое текстовых узлов, пробельных узлов и узлов-комментариев. Для чтения и записи. Для element имеет значение undefined.
Свойство textContent хранит весь текст внутри элемента (конкатенацию всех текстовых узлов). Текст внутри тэгов тоже берётся. Доступно для чтения и записи.
Свойство innerText возвращает текст в том виде, в котором он виден пользователю. (не стандартизировано)
Cвойство hidden отвечает за сокрытие элементов.
Существует много уникальных свойств: value для <input>
, href для <a>
.
Узнать свойства можно:
console.dir(element)
Полифилл (polyfill) – это библиотека, которая добавляет в старые браузеры поддержку возможностей, которые в современных браузерах являются встроенными.
Проверка поддержки свойства или метода: их сравние с undefined.
Добавить методы можно:
Element.prototype.method = function () {};
Добавить свойства можно:
Object.defineProperty(Element.prototype, 'property', { set: function(value) {}, get: function() {} });
При чтении HTML браузер генерирует DOM-модель. При этом большинство стандартных HTML-атрибутов становятся свойствами соответствующих объектов (но не все, поэтому если хотим получить то же, что в HTML, то обращаемся через атрибут).
Атрибуты – то, что написано в HTML (в тэгах элементов).
DOM-свойства – то, что находится внутри DOM-объекта.
Значение атрибутов - всегда строка, значение свойств - любое.
Названия свойств регистрозависимы, название атрибутов - нет.
Свойства не видны в innerHTML, атрибуты - видны.
Методы работы с атрибутами:
-
elem.hasAttribute('name')
– проверяет наличие атрибута -
elem.getAttribute('name')
– получает значение атрибута -
elem.setAttribute('name', 'value')
– устанавливает атрибут -
elem.removeAttribute('name')
– удаляет атрибут
Свойство elem.attributes возвращает коллекцию (объектов типа Attr) всех атрибутов элемента.
Синхронизация между HTML-атрибутами и DOM-свойствами чаще всего односторонняя: свойство зависит от атрибута, но не наоборот.
Пример:
input.value = '...'
не меняет атрибут input.getAttribute('value')
, но input.setAttribute('value', '...')
меняет input.value
(то есть даже несмотря на то, что на странице изменения отобразятся, атрибут будет по-прежнему хранить исходное значение).
Можно придумывать свои свойства и добавлять их, так как DOM-узлы являются JS-объектами, но это никак не должно повлиять на отображение в HTML.
Можно приписывать несуществующие или несвойственные (нестандартные) атрибуты HTML-элементам, но это не повлечёт за собой появляение соответствующих свойств, хоть эти атрибуты и их значения можно будет получить через свойство attributes
или метод getAttribute()
.
Можно использовать все виды атрибутов в css. Как стандартные: <input disabled> --> .input[disabled] {}
, так и нестандартные: <button red> --> .button[red]{} .button[blue]{}
, которые можно менять через elem.setAttribute()
.
Соответственно спецификации нестандартные атрибуты используются с припиской 'data-' в начале и называются data-атрибутами. В JS их можно получить с помощью свойства element.dataset: <div data-user-role> --> elem.dataset.userRole
.
Атрибуту class соответствует свойство className, являющееся строкой из классов элемента, и свойство classList, являющееся коллекцией из классов элемента, имеющей свои методы.
Методы объекта classList:
-
elem.classList.contains('class')
– возвращает true/false, в зависимости от того, есть ли у элемента класс class. -
elem.classList.add('class')
– добавляет класс. -
elem.classList.remove('class')
– удаляет класс. -
elem.classList.toggle('class')
– если класса class нет, добавляет его, если есть – удаляет.
Методы для проверки позиции:
-
parent.contains(child)
- проверяет вложенность. -
nodeA.compareDocumentPosition(nodeB)
проверяет относительную друг от друга позицию элементов (возвращает битовую маску).
Методы создания и удаления:
-
document.createElement('tag')
- создание элемента с указанным тэгом. -
document.createText('text')
- создание текстового узла с указанным текстом. -
parentElem.appendChild(elem)
- добавление элемента в конец списка детей родительского. -
parentElem.insertBefore(elem, next)
- вставка элемента в родительский перед элементом next (next === null --> insertBefore ~ appendChild). -
elem.cloneNode(true)
- глубокое копирование элемента (с атрибутами и подэлементами). -
elem.cloneNode(false)
- копирование элемента (без подэлементов). -
parentElem.removeChild(elem)
- удаление элемента из списка детей родительского. -
parentElem.replaceChild(newElem, elem)
- замена элемента новым элементом. -
elem.remove()
- удаление элемента напрямую (не требуя ссылки на родителя).
Вставка в document нужна для того, чтобы элемент отобразился. Методы вставки возвращают вставленный узел. Методы вставки автоматически удаляют элемент с предыдущего места, если он уже был вставлен в document. Методы удаления возвращают удалённый узел. При удалении элементов из childNodes, в нём всегда будет храниться текущее состояние (следующий элемент после удалённого встаёт на его место), поэтому обычный цикл for не сработает.
Метод insertAdjacentHTML вставляет произвольный HTML в любое место документа:
-
elem.insertAdjacentHTML('beforeBegin', html)
- перед elem. -
elem.insertAdjacentHTML('afterBegin', html)
- внутрь в начало elem. -
elem.insertAdjacentHTML('beforeEnd', html)
- внутрь в конец elem. -
elem.insertAdjacentHTML('afterEnd', html)
- после elem.
Методы insertAdjacentElement и insertAdjacentText работают аналогично.
Если использовать innerHTML += '...', которая не прибавляет, а заменяет всё содержимое, то все ресурсы будут перезагружены заново, все ссылки на предыдущие элементы станут не верны. К тому же этот способ позволяет вставлять только в конец.
DOM-объект DocumentFragment нужен в случае, если нужно вставить несколько DOM-узлов (какое-то поддерево) и при этом минимизировать количество операций с большим живым документом. Он похож на DOM-узел, но им не является, и у него нет обычных свойств DOM-узла. Посли вставки в DOM он исзечает, а на его место втавляются его дети.
Методы работы с DocumentFragment:
-
const fragment = document.createDocumentFragment()
- создание фрагмента. -
fragment.appendChild(node)
- наполнение фрагмента. -
fragment.cloneNode(true)
- глубокое клонирование фрагмента -
elem.appendChild(fragment)
- вставка фрагмента в DOM.
The DocumentFragment interface represents a minimal document object that has no parent. It is used as a lightweight version of Document that stores a segment of a document structure comprised of nodes just like a standard document. The key difference is that because the document fragment isn't part of the active document tree structure, changes made to the fragment don't affect the document. — developer.mozilla.org
Современные (эксперементальные) методы вставки/удаления:
-
node.append(...nodes)
– вставляет nodes в конец node. -
node.prepend(...nodes)
– вставляет nodes в начало node. -
node.after(...nodes)
– вставляет nodes после узла node. -
node.before(...nodes)
– вставляет nodes перед узлом node. -
node.replaceWith(...nodes)
– вставляет nodes вместо node.
Методы document.write и document.writeln пишут текст напрямую в текст документа (в HTML), до того как браузер построит из него DOM (отсюда и оптимизация: нет необходимости модифицировать DOM). Поэтому HTML записывается как есть (не валидируется и не исправляется в отличие от innerHTML), а затем браузер учитывает написанное при построении DOM. Использование методов после загрузке приведёт к перезаписи данных на странице. Часто используются для добавления скриптов с динамическим url (реклама).
Свойство style – объект, в котором CSS-свойства пишутся в lowerCamelCase. Чтение и изменение его свойств – работа с компонентами атрибута style. Чтобы обнулить поставленный стиль, достаточно присвоить ему пустую строку (в этом случае оно берётся из css).
Указанные в style величины измерения переводятся в стандартные для браузера и далее хранятся в них. Cпецифичные свойства браузеров пишутся в CamelCase: 'Webkit...', 'Moz...'.
Свойство style.cssText позволяет задавать свойства в виде строки. При установке этого свойства все предыдущие свойства style очищаются.
Метод getComputedStyle позволяет получить текущее используемое значение свойства:
-
getComputedStyle(element)
- получить стили элемента. -
getComputedStyle(element, 'pseudo')
- получить стили с учётом псевдо-элемента (например, псевдокласс :hover). -
getComputedStyle(element).property
- получить значение конкретного css-свойства элемента. -
getComputedStyle(element).getPropertyValue('property')
- то же самое.
Метод getComputedStyle работает только для полных свойств (полное - marginTop, неполное - margin). Также метод не даёт информацию о псевдоклассе :visited (посещена ли ссылка), чтобы защитить информацию о пользователях.
Когда браузер рисует страницу, то он высчитывает дерево расположения элементов, иначе говоря «дерево геометрии» или «дерево рендеринга», которое содержит всю информацию о размерах.
Метрики у элементов:
-
offsetParent
– «родитель по дереву рендеринга» – body для статического позиционирования или ближайший позиционированный элемент для других типов позиционирования. -
offsetLeft/offsetTop
– позиция в пикселях левого верхнего угла блока, относительно его offsetParent. -
offsetWidth/offsetHeight
– «внешняя» ширина/высота блока (полный размер, включая border). -
clientLeft/clientTop
– отступ области содержимого от левого-верхнего угла элемента. Если ОС располагает вертикальную прокрутку справа, то равны ширинам левой/верхней рамки, если же слева, то clientLeft включает в себя прокрутку. -
clientWidth/clientHeight
– ширина/высота содержимого. Включает в себя padding и не включает полосы прокрутки (видимая часть рассматриваемого элемента). -
scrollWidth/scrollHeight
– ширина/высота содержимого, включая прокручиваемую область. Включает в себя padding и не включает полосы прокрутки. -
scrollLeft/scrollTop
– ширина/высота прокрученной части документа, считается от верхнего левого угла.
Все эти свойства доступны только для чтения, кроме scrollLeft/scrollTop, изменение которых заставляет браузер прокручивать элемент.
Метрики для невидимых элементов равны нулю. Для элементов с display:none или находящихся вне документа дерево рендеринга не строится. Пустой div без ширины и высоты считается невидимым. Элементы за пределами экрана и элементы с visibility: 'hidden' считаются видимыми.
Лучше не использовать свойства width/height из getComputedStyle, поскольку они не во всех браузерах учитывают полосу прокрутки (альтернатива: clientWidth/clientHeight).