Skip to content
Sᴛѧʀʟɪɴɢ edited this page Feb 9, 2019 · 1 revision

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

Навигация по DOM

Если попытаться получить элемент, которого нет или пока нет в DOM, получим null.
К примеру, в <head> находится скрипт, в котором пытаемся получить доступ к <body>.

Дочерние элементы (или дети) – элементы, которые лежат непосредственно внутри данного.
Потомки – все элементы, которые лежат внутри данного, вместе с их детьми, детьми их детей и так далее. То есть, всё поддерево DOM.

DOM-коллекции (псевдомассивы) не являются JS-массивами (не имеют forEach, map и друшгих методов).

childNodes - коллекция, содержащая все дочерние узлы (включая текстовые).
children - коллекция, содержащая только дочерние узлы-элементы (соответствуют тегам).

Поиск по DOM

  • 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-селектору.

Иерархия DOM

Разные узлы являются объектами разных классов, поэтому имеют разные свойства.
Иерархия DOM

Примеры:<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() {} });

Атрибуты и DOM-свойства

При чтении 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 не сработает.

Мультивставка в DOM

Метод 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 (реклама).

Работа со стилями в DOM

Свойство 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 (посещена ли ссылка), чтобы защитить информацию о пользователях.

Метрики

Когда браузер рисует страницу, то он высчитывает дерево расположения элементов, иначе говоря «дерево геометрии» или «дерево рендеринга», которое содержит всю информацию о размерах.

metrix

Метрики у элементов:

  • 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).

Clone this wiki locally