JavaScript

Используем DOM как Pro

Spread the love

Статья написаны для тех кто постоянно пользуется jQuery. И не обращает на тот факт, что на настоящий момент для небольших проектов или проектах основанных на популярных фреймворках (React, Vue, Angular) нет необходимости использовать jQuery. Все операции с DOM можно делать используя нативное API.

В прошлые времена jQuery была очень популярна. Эта библиотека была, что называется «must have» и она использовалась почти в каждом проекте. И на то были свои причины. Нативное API для манипуляции с DOM было очень простым, и что бы совершить элементарные действия нужно было использовать несколько операторов. Но самое ужасное было в том, что в разных браузерах были свои нюансы использования API и они были не совместимы между собой. Поэтому была реальная необходимость в таких библиотеках и jQuery была лучшая из них. Она позволяла избавится от многословных строчек кода, и создавать простые и понятные, цепочки кода. Так же для нее было создано множество полезных плагинов.

Но в наше время 2019-2020 мир управляться фреймворками. Если вы начали изучать веб разработку в последнее десятилетие, есть вероятность, что вы возможно даже не столкнетесь с «нативным» DOM.

Наследие jQuery впечатляет, и отличным примером того, как эта библиотека повлияла на стандарты, являются методы querySelector и querySelectorAll, которые имитируют функцию $ в jQuery.

По иронии судьбы, эти два метода были, вероятно, главной причиной снижения популярности jQuery, поскольку они заменили наиболее используемую функциональность jQuery: простой выбор элементов DOM.

Но нативный DOM API все таки остался немного многословеным.

Я имею в виду, использование $ вместо document.querySelectorAll. Но тем не менее нативное DOM API великолепно и невероятно полезно. Да, оно многословно, но это потому, что это все такие низкоуровневые строительные блоки, предназначенные для построения абстракций. И если вы действительно беспокоитесь о дополнительных нажатиях клавиш: все современные редакторы и IDE обеспечивают отличное завершение кода. Вы также можете использовать псевдонимы наиболее часто используемых функций, как я покажу здесь.

И так давайте разберемся, как бы можем избавиться от jQuery!

Выбор элементов

Выбор одного элемента

Самый базовый оператор который нужно рассмотреть querySelector. Он позволяет выбрать один элемент, используется любой допустимый селектор CSS. По сути то аналог функции $ в jQuery. В старом DOM API использовался document.getElementById(‘myId’)

Пример использования:

document.querySelector(/* your selector */)

Вы можете использовать любой селектор:

document.querySelector('.my-class')       // выбор по классу
document.querySelector('#my-element')     // выбор по id
document.querySelector('div')             // выбор по тегу
document.querySelector('[name="my-name"]')// выбор по атрибуту
document.querySelector('div + p > span')  // выбор по комбинации тегов

Если соответствующий элемент не будет найден, он вернет null.

Выбор нескольких элементов

Для выбора нескольких элементов используется querySelectorAll. Аналог в jQuery функция $(document).find(‘div’). В старом DOM использовался document.getElementById(‘myId’).getElementsByTagName(‘li’);

Пример использования:

document.querySelectorAll('div')  // выберет все <div> элементы

Вы можете использовать document.querySelectorAll так же, как document.querySelector. Подойдет любой действительный селектор CSS, и единственное отличие состоит в том, что querySelector вернет один элемент, тогда как querySelectorAll вернет статический NodeList, содержащий найденные элементы. Если элементы не найдены, он вернет пустой NodeList.

NodeList — это итеративный объект, который похож на массив, но на самом деле это не массив, поэтому у него нет тех же методов. Вы можете запустить forEach на нем, но не map, reduce или find.

Если вам нужно запустить методы массива, вы можете просто превратить его в массив, используя […x] или Array.from:

// преобразование NodeList в массив
const arr = [...document.querySelectorAll('p')];

// или

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 может содержать только HTMLElements, а NodeList может содержать любой тип Node.

Нисходящий поиск элемента

Вам не обязательно запускать querySelectorAll для всего документа. Вы можете запустить его на любом элементе HTMLElement для запуска относительного поиска:

const div = document.querySelector('#my-element');
div.querySelectorAll('ul')  // вернет все <ul> в элементе #my-element

Это все еще многословно для просто поиска элемента!

Можно еще больше сократить использование document.querySelector:

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">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>');

Ну догадайся что? Есть нативный эквивалент insertAdjacentHTML:

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);

// Результат
<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);

Добавление класса

Для добавления класса используется функция classList. Аналог jQuery $(el).addClass(‘newClass’). Для добавления, удаления и переключения класса используется методы add, remove и toggle

let container = document.querySelector('#myId');
container.classList.add('newClass');
container.classList.remove('newClass');
container.classList.toggle('newClass');

Получение следующее элемента

В jQuery есть довольно часто используем оператор next(), он возвращает элемент, который следует непосредственно за текущим элементом наборе. В современном API для этого есть функция nextElementSibling

let next = document.querySelector('#myId').nextElementSibling;

Клонирование элемента

В jQuery для клонировния используется функция clone, например clonedEl = $(‘.el’).clone();

В новом API для этого есть cloneNode

let clonedElement = document.querySelector('.class').cloneNode(true);

Создание элемента из чистого 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('.my-class');  // false, нет класса "my-class"

Вы также можете проверить, является ли элемент дочерним по отношению к другому элементу с помощью метода 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.compareDocumentPosition(h1); 

// 10: контейнер содержит 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.

Объект options принимает следующие ключи:

  • attributes: если установлено значение true, изменения атрибутов узла будут отслеживаться
  • attributeFilter: массив имен атрибутов для просмотра, когда attributes имеют значение true, и если этот атрибут не установлен, будут отслеживаться изменения всех атрибутов узла
  • attributeOldValue: при значении true предыдущее значение атрибута будет записываться при каждом изменении
  • characterData: если установлено значение true, будет записывать изменения в тексте текстового узла, так как это работает только для текстовых узлов, а не для HTMLElements. Чтобы это работало, наблюдаемый узел должен быть node 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':
         // имя измененного атрибута находится в
         // 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'], // only observe attribute 'foo'
  attributeOldValue: true,
  childList: true
});

Когда вы закончите наблюдать, вы можете отключить наблюдателя и при необходимости вызвать его метод takeRecords, чтобы получить все ожидающие мутации, которые еще не были переданы в обратный вызов:

const mutations = observer.takeRecords();
callback(mutations);
observer.disconnect();

Заклчючение

DOM API — это невероятно мощное и универсальное, хотя и многословное API. Помните, что оно предназначен для того, чтобы предоставлять разработчикам низкоуровневые строительные блоки для построения абстракций, поэтому в этом смысле оно должен быть многословным, чтобы обеспечить однозначный и понятный API.

Дополнительные нажатия клавиш не должны пугать вас от использования его в полной мере.

DOM является необходимым знанием для каждого разработчика JavaScript, поскольку вы, вероятно, используете его каждый день. Не бойтесь этого и используйте его в полной мере.

В интернете можно найти множество сайтов показывающие аналоги функций jquery в нативном JS, например: http://youmightnotneedjquery.com/

Источники:

Danny MoerkerkeUsing the DOM like a Pro
10 методов для замены jQuery чистым JavaScript в проектах
Нативные аналоги jQuery 

Была ли вам полезна эта статья?
[14 / 5]

Spread the love
DenSP

View Comments

  • > document.querySelector('p').closest('div');

    А теперь real life пример:

    > document.querySelector('.my-awesome-class').closest('.my-another-class').closes('div');

    Если кодин из классов не существует на данный момент в DOM, то `Cannot read property 'closest' of null`
    В jQuery вернулась бы обёртка с длиной === 0

    • Ничто не мешает написать свое функцию обертку с дополнительными проверками. closest все же низкоуровневое API.
      В статье ни говориться что ни нужно пользоваться jQuery, в ней говориться что не плохо бы знать низкоуровневое API и при возможности его использовать (когда это возможно), а не тащить jQuery во всех возможных ситуациях.

Recent Posts

Vue 3.4 Новая механика v-model компонента

Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование​ v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…

12 месяцев ago

Анонс Vue 3.4

Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…

12 месяцев ago

Как принудительно пере-отобразить (re-render) компонент Vue

Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…

2 года ago

Проблемы с установкой сертификата на nginix

Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…

2 года ago

Введение в JavaScript Temporal API

Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…

2 года ago

Когда и как выбирать между медиа запросами и контейнерными запросами

Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…

2 года ago