Документ

Навигация по DOM-элементам

Свойства узлов

Свойства для поика любых узлов

childNodes возвращает NodeList всех дочерних узлов, включая текстовые.

firstChild, lastChild – первый и последний дочерние узлы.

previousSibling, nextSibling – соседи-узлы.

parentNode – родитель-узел.

Свойства для поиска узлов-элементов

children возвращает HTMLCollection всех дочерних узлов-элементов.

firstElementChild, lastElementChild – первый и последний дочерние элементы.

previousElementSibling, nextElementSibling – соседи-элементы.

parentElement – родитель-элемент.

Отличие parentElement от parentNode:

document.documentElement.parentElement === null document.documentElement.parentNode === document
Таблицы

table.rowsHTMLCollection строк <tr> таблицы.

tr.cells - HTMLCollection <td> и <th> ячеек, находящихся внутри строки <tr>.

table.tBodiesHTMLCollection элементов таблицы <tbody>.

table.caption/tHead/tFoot – ссылки на элементы таблицы <caption>, <thead>, <tfoot>.

tr.sectionRowIndex – номер строки <tr> в текущей секции <thead>/<tbody>/<tfoot>.

tr.rowIndex – номер строки <tr> в таблице (включая все строки таблицы).

td.cellIndex – номер ячейки в строке <tr>.

HTLMCollection vs NodeList

"Живая" коллекция - всегда отражает текущее состояние документа и автоматически обновляется при его изменении.

HTMLCollection - всегда "живая" коллекция и хранит только узлы-элементы.

NodeList может быть как "живой", так и статической коллекцией и хранит любые типы узлов. Есть методы forEach(), entries(), keys() и values().

childNodes и document.getElementsByName() возвращают "живую" коллекцию NodeList.

querySelectorAll() возвращает статическую коллекцию NodeList.

let elems = document.body.childNodes; console.log(elems.constructor === NodeList); // true console.log(elems.length); // 1 document.body.append( document.createElement('div') ); console.log(elems.length); // 2

Поиск: getElement*, querySelector*

document.getElementById - ищет элемент по идентификатору id.

document.getElementsByName - возвращает NodeList элементов с заданным атрибутом name.

elem.querySelectorAll - возвращает NodeList элементов, удовлетворяющих CSS-селектору.

elem.querySelector - возвращает первый элемент, соответствующий CSS-селектору.

elem.getElementsByClassName - возвращает HTMLCollection элементов с заданным классом.

elem.getElementsByTagName - возвращает HTMLCollection элементов с заданным тегом. Передав "*" вместо тега, возвращает всех потомков.

Другие методы

elem.matches(css) - вернет true, если elem удовлетворяет CSS-селектору, иначе false.

elem.closest(css) - возвращает ближайшего предка (включая себя), соответствующего CSS-селектору.

elemA.contains(elemB) - вернет true, если elemB потомок elemA, либо elemA === elemB, иначе false.

Атрибуты и свойства узлов

Тип, тег и содержимое

Каждый DOM-узел принадлежит соответствующему встроенному классу. Корнем иерархии является EventTarget. От него наследует Node, от которого наследуют остальные узлы.

Наследование основных классов
Свойство «nodeType»

elem.nodeType === 1 для узлов-элементов.

elem.nodeType === 3 для текстовых узлов.

elem.nodeType === 8 для узлов-комментариев.

elem.nodeType === 9 для объектов документа.

Тег: nodeName и tagName

Свойство tagName есть только у элементов, наследующих от Element. Возвращает тег элемента в верхнем регистре.

Свойство nodeName определено для любых узлов Node. Для элементов равно tagName, для остальных узлов содержит строку с типом узла.

document.body.tagName // 'BODY' document.body.firstChild.nodeName // '#text'
innerHTML: содержимое элемента

Свойство innerHTML позволяет получить HTML-содержимое элемента в виде строки.

outerHTML: HTML элемента целиком

Запись в outerHTML не изменяет элемент, а удаляет его из внешнего контекста и вставляет вместо него новый HTML-код.

HTML: <div>Привет, мир!</div>

let div = document.querySelector('div'); div.outerHTML = '<p>Новый элемент</p>'; console.log(div.outerHTML); // <div>Привет, мир!</div>
nodeValue/data: содержимое текстового узла

Свойства nodeValue и data возвращают содержимое узла, если узел не является элементом. Иначе nodeValue возвращает null, а data - undefined.

textContent: просто текст

Возвращает содержимое любого узла за вычетом всех тегов. При записи в textContent записывает HTML-теги как обычный текст.

Свойство «hidden»

Атрибут и DOM-свойство hidden работает почти так же, как style="display:none".

HTML: <div id="elem">Мигающий элемент</div>

setInterval(() => elem.hidden = !elem.hidden, 1000);

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

HTML-атрибуты

Когда браузер парсит HTML, чтобы создать DOM-объекты для тегов, он распознаёт стандартные атрибуты и создаёт DOM-свойства для них. Этого не происходит, если атрибут нестандартный.

HTML: <body id="test" something="non-standard">

console.log(document.body.id); // test console.log(document.body.something); // undefined

DOM-узлы имеют свойства, зависящие от класса. Поэтому стандартный атрибут для одного тега может быть нестандартным для другого.

Все атрибуты доступны с помощью следующих методов:

elem.hasAttribute(name) – проверяет наличие атрибута.

elem.getAttribute(name) – получает значение атрибута.

elem.setAttribute(name, value) – устанавливает значение атрибута.

elem.removeAttribute(name) – удаляет атрибут.

Кроме того, elem.attributes возвращает коллекцию всех атрибутов элемента. Каждый атрибут имеет свойства name и value.

for (let attr of elem.attributes) {} // все атрибуты elem по порядку
HTML-атрибуты vs DOM-свойства

DOM-свойства и методы - обычные объекты JavaScript.

Имена HTML-атрибутов регистронезависимы, а значения - всегда строки.

Синхронизация между атрибутами и свойствами

Когда стандартный атрибут изменяется, соответствующее свойство автоматически обновляется. Это работает и в обратную сторону (за некоторыми исключениями).

input.value синхронизируется только в одну сторону: атрибут → значение. Изменение атрибута value обновит свойство, но изменение свойства не повлияет на атрибут.

DOM-свойства типизированы

DOM-свойства могут отличаться от атрибутов:

Свойство input.checked (для чекбоксов) имеет логический тип.

Атрибут style – строка, но свойство style является объектом.

Свойство href всегда содержит полный URL, даже если атрибут содержит относительный URL или просто #hash.

Нестандартные атрибуты, dataset

Все атрибуты, начинающиеся с префикса data-, зарезервированы для использования программистами. Они доступны в свойстве dataset.

Атрибуты, состоящие из нескольких слов, к примеру data-order-state, становятся свойствами, записанными с помощью верблюжьей нотации: dataset.orderState.

Изменение документа

Создание элемента

document.createElement(tag) - создаёт новый элемент с заданным тегом.

document.createTextNode(text) - создаёт новый текстовый узел с заданным текстом.

Методы вставки

node.append(...nodes or strings) – добавляет узлы или строки в конец node.

node.prepend(...nodes or strings) – вставляет узлы или строки в начало node.

node.before(...nodes or strings) – вставляет узлы или строки до node.

node.after(...nodes or strings) – вставляет узлы или строки после node.

node.replaceWith(...nodes or strings) – заменяет node заданными узлами или строками.

insertAdjacentHTML

elem.insertAdjacentHTML(where, html) - вставляет html как HTML-код. where может иметь значения:

"beforebegin" – вставить html перед elem.

"afterbegin" – вставить html в начало elem.

"beforeend" – вставить html в конец elem.

"afterend" – вставить html после elem.

elem.insertAdjacentText(where, text) - вставляет text как текст.

elem.insertAdjacentElement(where, elem) - вставляет elem как элемент.

Удаление узлов

node.remove() - удаляет узел.

Все методы вставки автоматически удаляют узлы со старых мест.

Клонирование узлов: cloneNode

elem.cloneNode(truthy) - создает глубокую копию elem со всеми дочерними элементами.

elem.cloneNode(falsy) - создает поверхностную копию elem без дочерних элементов.

DocumentFragment

new DocumentFragment - создает специальный DOM-узел, который служит обёрткой для передачи списков узлов. При вставке куда-либо, он «исчезает», вместо него вставляется его содержимое.

Стили и классы

className и classList

elem.className – строковое свойство, возвращает все классы элемента через пробел. При присваивании заменяет все существующие классы на новые.

document.body.getAttribute('class'); // 'class1' document.body.className = 'class2 class3'; document.body.getAttribute('class'); // 'class2 class3'

elem.classList – это специальный объект с методами для добавления/удаления одного класса.

elem.classList.add/remove("class") – добавить/удалить класс.

elem.classList.toggle("class") – добавить класс, если его нет, иначе удалить.

elem.classList.contains("class") – проверка наличия класса, возвращает true/false.

Кроме того, classList является перебираемым, поэтому можно перечислить все классы в цикле for..of.

Element style

Свойство style является объектом со стилями в формате camelCase. Чтение и запись в него работают так же, как изменение соответствующих свойств в атрибуте "style".

document.body.style.backgroundColor = prompt('background color?', 'green');

Сброс стилей

При присвоении пустой строки в качестве значения свойства, браузер применит встроенные стили, как если бы такого свойства не было.

document.body.style.display = "none"; // скрыть setTimeout(() => document.body.style.display = "", 1000); // возврат к первоначальному состоянию

elem.style.cssText позволяет задать сразу несколько стилей в виде строки, осуществляя перезапись всех стилей в атрибуте 'style'. elem.setAttribute('style', '...') работает так же.

document.body.style.height = '100px'; console.log( document.body.getAttribute('style') ); // 'height: 100px;' document.body.style.cssText = 'width: 200px; background: yellow'; console.log( document.body.getAttribute('style') ); // 'width: 200px; background: yellow;'

Вычисленные стили: getComputedStyle

getComputedStyle(element, [pseudo]) - возвращает объект, содержащий значения всех CSS-свойств элемента, полученных после применения всех стилей и вычисления их значений.

element - элемент, значения для которого нужно получить.

pseudo - указывается, если нужен стиль псевдоэлемента, например, '::before'. Пустая строка или отсутствие аргумента означают сам элемент.

getComputedStyle требует полное свойство, например, paddingLeft, borderTopWidth. При обращении к сокращённому правильный результат не гарантируется, так как стандарта для этого нет.

Размеры и прокрутка элементов

Метрики

Существует множество JavaScript-свойств, которые позволяют получить геометрические характеристики элемента: ширину, высоту и тд. В этой главе они будут называться «метрики».

Метрики

offsetParent, offsetLeft/offsetTop

Свойство offsetParent содержит ближайший предок элемента, удовлетворяющий следующим условиям:

  1. Является CSS-позиционированным (CSS-свойство position равно absolute, relative, fixed или sticky).
  2. Или <td>, <th>, <table>.
  3. Или <body>.

Ситуации, когда offsetParent равно null:

  1. Для скрытых элементов (с CSS-свойством display:none или когда его нет в документе).
  2. Для элементов <body> и <html>.
  3. Для элементов с position:fixed.

Свойства offsetLeft/offsetTop содержат координаты x/y относительно верхнего левого угла offsetParent (0, если offsetParent === null).

offsetWidth/offsetHeight

Свойства offsetWidth/offsetHeight cодержат «внешнюю» ширину/высоту элемента, то есть его полный размер, включая рамки.

Если элемент (или любой его родитель) имеет display:none или отсутствует в документе, то все его метрики равны нулю.

clientTop/clientLeft

Свойства clientLeft/clientTop возвращают отступы внутренней части элемента от внешней.

Отступ внутренней части от внешней определяется рамками border и полосой прокрутки (в операционной системе на арабском языке или иврите полоса прокрутки расположена слева, а не справа).

clientWidth/clientHeight

Свойства clientWidth/clientHeight содержат ширину/высоту содержимого элемента вместе с внутренними отступами padding.

scrollWidth/Height

Свойства scrollWidth/scrollHeight содержат ширину/высоту содержимого элемента, включая невидимую область (скролл).

Если полосы прокрутки нет, свойства scrollWidth/scrollHeight и clientWidth/clientHeight равны.

Эти свойства можно использовать, чтобы «распахнуть» элемент на всю ширину/высоту:

elem.style.height = `${elem.scrollHeight}px`;

scrollLeft/scrollTop

Свойства scrollLeft/scrollTop содержат ширину/высоту невидимой, уже прокрученной части содержимого элемента.

Свойства scrollLeft/scrollTop можно изменять. Установка значения scrollTop на 0 или на большое значение, такое как 1e9, прокрутит элемент в самый верх/низ соответственно.

Метод scroll делает то же самое. В качестве параметра он принимает объект со свойствами left, top и behavior:

elem.scroll({ left: 0, // свойство можно не указывать, если оно равно нулю top: 500, behavior: 'smooth', });

Не стоит брать width/height из CSS

  1. CSS-свойства width/height зависят от другого свойства – box-sizing.
  2. В CSS свойства width/height могут быть равны auto (например, для инлайнового элемента).
  3. С getComputedStyle().width/height могут возникать кроссбраузерные отличия. Если есть полоса прокрутки, некоторые браузеры вычитают ее ширину из CSS-ширины, а некоторые – нет.

Размеры и прокрутка окна

Ширина/высота окна

window.innerWidth/innerHeight указывают на ширину/высоту видимой части документа, включая полосу прокрутки.

document.documentElement.clientWidth/clientHeight указывают на ширину/высоту видимой части документа, доступной для содержимого.

Ширина/высота документа

Теоретически, полный размер документа можно получить как documentElement.scrollWidth/scrollHeight.

Однако, на этом элементе эти свойства могут работать иначе. Чтобы надёжно получить полную высоту документа, следует взять максимальное из этих свойств:

let scrollHeight = Math.max( document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight );

Получение текущей прокрутки

Свойства window.scrollX/window.scrollY содержат текущую прокрутку документа.

window.pageXOffset/window.pageYOffset - устаревшие свойства для поддержки IE.

Прокрутка: scrollTo, scrollBy, scrollIntoView

Метод scrollBy(x, y) прокручивает страницу относительно её текущего положения.

Метод scrollTo(pageX, pageY)/scrollTo(options) прокручивает страницу на абсолютные координаты, заданные pageX, pageY или объектом options со свойствами left, top, behavior.

Вызов elem.scrollIntoView(top) прокручивает страницу так, чтобы elem оказался виден. Значения top:

  • если truthy или undefined (без аргумента), совмещает верхний край elem с верхним краем окна
  • если falsy (но не undefined), совмещает нижний край elem с нижним краем окна

elem.scrollIntoView(viewOptions) может принимать объект со свойствами:

  • behavior - smooth/instant/auto, анимация скролла
  • block - [start]/center/end/nearest, вертикальное выравнивание
  • inline - start/center/end/[nearest], горизонтальное выравнивание

Координаты

Для получения координат элемента относительно документа, необходимо текущую прокрутку документа window.scrollX/window.scrollY сложить с координатами относительно окна.

Координаты относительно окна: getBoundingClientRect

Метод elem.getBoundingClientRect() возвращает объект с координатами элемента относительно окна. Свойства:

x/y – X/Y-координаты начала прямоугольника относительно окна.

width/height – ширина/высота прямоугольника.

Дополнительные («зависимые») свойства:

top/bottom – Y-координата верхней/нижней границы прямоугольника.

left/right – X-координата левой/правой границы прямоугольника.

elementFromPoint(x, y)

Вызов document.elementFromPoint(x, y) возвращает самый глубоко вложенный элемент в окне, находящийся по координатам x, y. Для координат за пределами окна метод возвращает null.

Введение в события

Введение в браузерные события

Обработчики событий

Событие – это сигнал от браузера о том, что что-то произошло.

Событию можно назначить обработчик, то есть функцию, которая сработает, как только событие произошло.

Внутри обработчика this ссылается на тот элемент, на который обработчик был назначен.

Использование атрибута HTML

Обработчик может быть назначен в HTML-разметке, в атрибуте on<событие>.

HTML: <button onclick="console.log('hey')">Нажми на меня</button>

В атрибут можно передать отдельную JavaScript-функцию.

HTML: <button onclick="countRabbits()">Считать кроликов!</button>

function countRabbits() { if (!countRabbits.counter) countRabbits.counter = 0; console.log("Кролик " + ++countRabbits.counter); }
Использование свойства DOM-объекта

Можно назначать обработчик, используя свойство DOM-элемента on<событие>.

HTML: <input id="elem" type="button" value="Нажми меня!">

elem.onclick = () => console.log('Спасибо');

Этот способ, по сути, аналогичен предыдущему. Если обработчик задан через атрибут, то браузер читает HTML-разметку, создаёт новую функцию из содержимого атрибута и записывает в свойство.

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

Убрать обработчик можно назначением elem.onclick = null.

addEventListener

Метод addEventListener позволяет добавлять несколько обработчиков на одно событие.

Обработчики некоторых событий можно назначать только через addEventListener.

elem.addEventListener(event, handler, [options]) - добавление обработчика с необязательным объектом options.

elem.addEventListener(event, handler, useCapture) - альтернативный вариант.

event - имя события, например "click".

handler - ссылка на функцию-обработчик.

options - дополнительный объект со свойствами:

  • once: [false]/true - будет ли обработчик удалён после выполнения
  • capture: [false]/true - фаза, на которой сработает обработчик
  • passive: [false]/true - указать браузеру, что обработчик никогда не вызовет preventDefault()

useCapture - то же, что и options.capture.

Удаление обработчика требует ту же функцию-обработчик и фазу capture.

elem.removeEventListener(event, handler, [options]) - первый способ.

elem.removeEventListener(event, handler, useCapture) - второй способ.

Объект события

Когда происходит событие, браузер создаёт объект события, записывает в него детали и передаёт его в качестве аргумента функции-обработчику.

Некоторые свойства объекта события:

type - тип события, например, "click".

currentTarget - элемент, на котором сработал обработчик.

clientX/clientY - координаты курсора в момент события относительно окна, для событий мыши.

Объект события доступен и в HTML:

HTML: <button onclick="console.log(event)">Нажми меня!</button>

Это возможно потому, что когда браузер считывает атрибут on*, например onclick, он создаёт функцию-обработчик с содержимым этого атрибута в качестве тела функции: function(event) { console.log(event) }

Объект-обработчик: handleEvent

addEventListener поддерживает объекты в качестве обработчиков событий. Когда происходит событие, вызывается метод объекта handleEvent.

HTML: <input id="elem" type="button" value="Нажми меня!">

elem.addEventListener('click', { handleEvent(e) { console.log(e.type + " на " + e.currentTarget); } });

Всплытие и погружение

Всплытие (bubbling)

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

Почти все события всплывают. Исключением является, например, событие focus.

event.target

Самый глубоко вложенный элемент, который вызывает событие, доступен через event.target.

Прекращение всплытия

Всплытие идёт с «целевого» элемента event.target прямо наверх. Событие будет всплывать вплоть до глобального объекта window, вызывая все обработчики на своём пути. Но любой промежуточный обработчик может остановить всплытие.

event.stopPropagation() - прекращает всплытие события.

Если у элемента есть несколько обработчиков на одно событие, то даже при прекращении всплытия все они будут выполнены.

event.stopImmediatePropagation() - прекращает всплытие и станавливает обработку событий на текущем элементе.

Погружение (capturing)

Стандарт DOM Events описывает 3 фазы прохода события:

  1. Capturing phase – фаза погружения.
  2. Target phase – фаза цели.
  3. Bubbling phase – фаза всплытия.

Свойство объекта события eventPhase содержит номер фазы, на которой событие было поймано.

Фазы события

Чтобы поймать событие на стадии погружения, необходимо назначить обработчик через addEventListener, указав capture: true.

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

Делегирование событий

Всплытие событий позволяет реализовать один из самых важных приёмов разработки – делегирование.

Если есть много элементов, события на которых нужно обрабатывать схожим образом, то обработчик ставится на их общего предка. Из него можно получить целевой элемент event.target, понять на каком именно потомке произошло событие и обработать его.

table.onclick = function(event) { let td = event.target.closest('td'); if (!td) return; if (!table.contains(td)) return; td.classList.add('highlight'); };

Действия браузера по умолчанию

Многие события автоматически влекут за собой действие браузера. Если событие обрабатывается в JavaScript, то зачастую такое действие браузера не нужно.

Отмена действия браузера

Есть два способа отменить действие браузера:

  1. Вызвать event.preventDefault()
  2. Если обработчик назначен через on<событие>, то также можно вернуть false из обработчика.

HTML: <a href="/" onclick="return false">Нажми здесь</a>

HTML: <a href="/" onclick="event.preventDefault()">Или тут</a>

Опция «passive» для обработчика

Необязательная опция passive: true для addEventListener сигнализирует браузеру, что обработчик не собирается выполнять preventDefault().

Некоторые события, такие как touchmove на мобильных устройствах (когда пользователь перемещает палец по экрану), по умолчанию начинают прокрутку. С опцией passive: true браузер начинает прокрутку немедленно, обеспечивая максимально плавный интерфейс и параллельно обрабатывая событие.

event.defaultPrevented

Свойство event.defaultPrevented установлено в true, если действие по умолчанию было предотвращено, и false, если нет.

Например, по умолчанию браузер при событии contextmenu показывает контекстное меню со стандартными опциями. Можно отменить событие по умолчанию и показать что-то своё:

HTML: <button id="elem">Нажми меня!</button>

elem.oncontextmenu = function(event) { event.preventDefault(); console.log("Контекстное меню кнопки"); }; document.oncontextmenu = function(event) { if (event.defaultPrevented) return; event.preventDefault(); console.log("Контекстное меню документа"); };

Генерация пользовательских событий

Конструктор Event

let event = new Event(type[, options]) - создание события встроенного класса Event.

type – тип события, строка, например, "click" или придуманный нами "my-event".

options – объект с тремя необязательными свойствами:

  • bubbles: true/[false] – будет ли событие всплывать.
  • cancelable: true/[false] – возможность отменить действие по умолчанию.
  • composed: true/[false] – всплытие события наружу за пределы Shadow DOM.
Метод dispatchEvent

После того, как объект события создан, запустить его на элементе можно, вызвав метод elem.dispatchEvent(event).

Свойство event.isTrusted принимает значение true для событий, порождаемых реальными действиями пользователя, и false для генерируемых кодом.

Пример всплытия

Создадим всплывающее событие с именем "hello" и поймаем его на document. Для этого необходимо установить флаг bubbles в true.

HTML: <h1 id="elem">Привет из кода!</h1>

document.addEventListener("hello", function(e) { console.log("Привет от " + e.target.tagName); // Привет от H1 }); let event = new Event("hello", {bubbles: true}); elem.dispatchEvent(event);

on<event>-свойства существуют только для встроенных событий, поэтому необходимо использовать addEventListener.

MouseEvent, KeyboardEvent и другие

Для некоторых типов событий есть свои конструкторы (UIEvent, FocusEvent, MouseEvent, WheelEvent, KeyboardEvent и тд), которые лучше использовать вместо Event, что позволит указать стандартные свойства для данного типа события.

let event = new MouseEvent("click", { bubbles: true, cancelable: true, clientX: 100, clientY: 100 }); console.log(event.clientX); // 100

При создании через обычный конструктор Event мы получили бы undefined.

Пользовательские события

Для генерации событий совершенно новых типов, таких как "hello", следует использовать конструктор CustomEvent. В дополнительном свойстве detail можно указать информацию для передачи в событие.

HTML: <h1 id="elem">Привет для Васи!</h1>

elem.addEventListener("hello", function(event) { console.log(event.detail.name); // Вася }); elem.dispatchEvent(new CustomEvent("hello", { detail: {name: "Вася"} }));
event.preventDefault()

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

Вызов event.preventDefault() является возможностью для обработчика события сообщить в сгенерировавший событие код, что эти действия надо отменить (событие должно содержать флаг cancelable: true). Тогда вызов elem.dispatchEvent(event) возвратит false.

Веб-компоненты

Custom Elements

Мы можем создавать пользовательские HTML-элементы, описываемые нашим классом, со своими методами и свойствами, событиями и так далее.

Существует два вида пользовательских элементов:

  1. Автономные пользовательские элементы – «полностью новые» элементы, расширяющие абстрактный класс HTMLElement.
  2. Пользовательские встроенные элементы – элементы, расширяющие встроенные, например кнопку HTMLButtonElement и т.п.

Схема определения автономного пользовательского элемента:

class MyElement extends HTMLElement { constructor() { super(); /* ... */ } connectedCallback() { /* ... */ } disconnectedCallback() { /* ... */ } static get observedAttributes() { return [/* ... */]; } attributeChangedCallback(name, oldValue, newValue) { /* ... */ } adoptedCallback() { /* ... */ } /* ... наши методы и свойства ... */ }

connectedCallback() - вызывается при добавлении элемента в документ.

disconnectedCallback() - вызывается при удалении элемента из документа.

static get observedAttributes() - массив имён атрибутов для отслеживания их изменений.

attributeChangedCallback() - вызывается при изменении одного из перечисленных выше атрибутов.

adoptedCallback() - вызывается, когда элемент перемещается в новый документ (используется редко).

customElements.define("my-element", MyElement) - "регистрирует" элемент, то есть сообщает браузеру, что <my-element> обслуживается классом MyElement. Для любых HTML-элементов с тегом <my-element> будет создаваться экземпляр MyElement и вызываться вышеупомянутые методы.

Имя пользовательского элемента должно содержать дефис - чтобы гарантировать отсутствие конфликтов имён между встроенными и пользовательскими элементами HTML.

Если браузер сталкивается с пользовательским элементом до вызова customElements.define, элемент будет неизвестен, как и любой нестандартный тег. «Неопределённые» пользовательские элементы могут быть стилизованы с помощью CSS селектора :not(:defined).

customElements.get(name) – возвращает конструктор пользовательского элемента с указанным именем name.

customElements.whenDefined(name) – возвращает промис, который переходит в состояние fulfilled со значением конструктора элемента, когда определён пользовательский элемент с указанным именем name.

Порядок рендеринга

Когда HTML-парсер строит DOM, элементы обрабатываются друг за другом, родители до детей. Например, если есть <outer><inner></inner></outer>, то элемент <outer> создаётся и включается в DOM первым, а затем <inner>. Из-за этого если пользовательский элемент попытается получить доступ к innerHTML в connectedCallback, он ничего не получит. Если действительно нужны дочерние элементы, можно отложить доступ к ним, используя setTimeout с нулевой задержкой.

Модифицированные встроенные элементы

Встроенные HTML-элементы можно расширять и модифицировать, наследуя их классы. Для этого требуется указать еще один аргумент в customElements.define и атрибут is в HTML.

class HelloButton extends HTMLButtonElement { /* ... */ } customElements.define('hello-button', HelloButton, {extends: 'button'});

HTML: <button is="hello-button">...</button>

Shadow DOM

Теневой DOM используется для инкапсуляции.

Каждый DOM-элемент может иметь 2 типа поддеревьев DOM:

  1. Light tree – обычное, «светлое», DOM-поддерево, состоящее из HTML-потомков.
  2. Shadow tree – скрытое, «теневое», DOM-поддерево, не отражённое в HTML.

Если у элемента имеются оба поддерева, браузер отрисовывает только теневое дерево.

elem.attachShadow({mode: …}) создаёт теневое дерево и возвращает объект, являющийся корнем дерева. С этим объектом можно работать как с обычным DOM-элементом.

В качестве elem может быть использован пользовательский элемент, либо один из следующих элементов: article, aside, blockquote, body, div, footer, h1…h6, header, main, nav, p, section, span.

Свойство mode задаёт уровень инкапсуляции. У него может быть два значения:

  • "open" – корень теневого дерева («shadow root») доступен как elem.shadowRoot. Любой код может получить теневое дерево elem.
  • "closed" – elem.shadowRoot всегда возвращает null. До теневого DOM в таком случае добраться можно только по ссылке, которую возвращает attachShadow (скорее всего, она будет спрятана внутри класса).

Элемент с корнем теневого дерева называется – «хозяин» (host) теневого дерева, и он доступен в качестве свойства host у shadow root.

console.log(elem.shadowRoot.host === elem); // true

Инкапсуляция

Теневой DOM отделён от главного документа:

  1. Элементы теневого DOM не видны из обычного DOM через querySelector. В частности, элементы shadow DOM могут иметь такие же идентификаторы, как у элементов в light DOM. Они должны быть уникальными только внутри теневого дерева.
  2. У теневого DOM свои стили. Стили из внешнего DOM не применятся.

HTML: <div id="elem"></div>

elem.attachShadow({mode: 'open'}); elem.shadowRoot.innerHTML = ` <style> p { font-weight: bold; } </style> <p>Hello, John!</p> `; console.log(document.querySelectorAll('p').length); // 0 console.log(elem.shadowRoot.querySelectorAll('p').length); // 1

Элемент "template"

Встроенный элемент <template> предназначен для хранения шаблона HTML. Браузер полностью игнорирует его содержимое, проверяя лишь синтаксис. Содержимое <template> считается находящимся «вне документа», поэтому ни на что не влияет.

Содержимым <template> может быть любой корректный HTML-код, даже такой, который обычно нуждается в специальном родителе (например <tr>). Также внутри <template> можно поместить стили и скрипты.

Использование template

Содержимое шаблона доступно по его свойству content в качестве узла DocumentFragment. При вставке его куда-либо вставляется не он сам, а его дети. template.content можно клонировать и переиспользовать в новом компоненте.

let elem = document.createElement('div'); elem.append(tmpl.content.cloneNode(true)); document.body.append(elem);

HTML:

<template id="tmpl"> <script>console.log("Привет");</script> <style>p {font-weight: bold; }</style> <p class="message">Привет, Мир!</p> </template>

При вставке в документ содержимое <template> оживает (скрипты выполняются, <video autoplay> проигрывается и тд).

Слоты теневого DOM, композиция

Теневой DOM поддерживает элементы <slot>, которые автоматически наполняются контентом из обычного, «светлого» DOM-дерева.

Именованные слоты

В теневом DOM элемент <slot name="X"> определяет «точку вставки» – место, где отображаются элементы с атрибутом slot="X". Затем браузер выполняет «композицию»: берёт элементы из обычного DOM-дерева и отображает их в внутри соответствующих слотов теневого DOM-дерева. В результате получается компонент, который можно наполнить данными. При композиции не происходит перемещения узлов – DOM остаётся прежним.

customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = ` <div>Имя: <slot name="username"></slot> </div> <div>Дата рождения: <slot name="birthday"></slot> </div> `; } });

HTML:

<user-card> <span slot="username">Иван Иванов</span> <span slot="birthday">01.01.2001</span> </user-card>

Атрибут slot="…" могут иметь только дети первого уровня. Для вложенных элементов он игнорируется.

Если в светлом DOM есть несколько элементов с одинаковым именем слота, они добавляются в слот один за другим.

Содержимое слота «по умолчанию»

Если добавить данные в <slot>, это становится содержимым «по умолчанию». Браузер отображает его, если в светлом DOM-дереве отсутствуют данные для заполнения слота.

HTML:

<div>Имя: <slot name="username">Аноним</slot> </div>
Слот по умолчанию (первый без имени)

Первый <slot> в теневом дереве без атрибута name является слотом по умолчанию. Он будет отображать данные со всех узлов светлого дерева, не добавленные в другие слоты.

Обновление слотов

Браузер наблюдает за слотами и обновляет отображение при добавлении и удалении элементов в слотах.

slotchange – событие, которое запускается, когда слот наполняется контентом в первый раз, и при каждой операции добавления/удаления/замещения элемента в слоте, за исключением его потомков. Сам слот можно получить как event.target.

shadowRoot не может иметь обработчиков событий, поэтому следует использовать первый дочерний элемент:

this.shadowRoot.firstElementChild.addEventListener('slotchange', e => console.log("slotchange: " + e.target.name) );

Для более глубокого просмотра содержимого элемента в слоте и отслеживания изменений в нём можно использовать MutationObserver.

API слотов

Если у теневого дерева стоит {mode: 'open'}, то можно выяснить, какие элементы находятся в слоте, и, наоборот, определить слот по элементу, который в нём находится:

node.assignedSlot – возвращает элемент <slot>, в котором находится node.

slot.assignedNodes(options) – возвращает DOM-узлы, которые находятся в слоте.

slot.assignedElements(options) – возвращает DOM-элементы, которые находятся в слоте.

options - необязательный объет со свойством flatten: [false]/true. Если явно изменить значение на true, методы просматривают развёрнутый DOM глубже и возвращает вложенные слоты, если есть вложенные компоненты, и резервный контент, если в слоте нет узлов.

Настройка стилей теневого DOM

Теневой DOM может включать в себя стили, как <style> или <link rel="stylesheet">. Как правило, локальные стили работают только внутри теневого DOM, а стили документа – вне его. Но есть несколько исключений.

:host

Селектор :host позволяет выбрать элемент-хозяин (элемент, содержащий теневое дерево).

Например, создается элемент <custom-dialog> который нужно расположить по-центру. Для этого необходимо стилизовать сам элемент <custom-dialog>. Это именно то, что делает :host.

Каскадирование

Элемент-хозяин находится в светлом DOM, поэтому к нему применяются CSS-стили документа.

Если есть некоторое свойство, стилизованное как в :host локально, так и в документе, то стиль документа будет приоритетным (исключение - когда локальное свойство помечено как !important).

Это очень удобно, поскольку можно задать стили «по умолчанию» в компоненте в его правиле :host, а затем легко переопределить их в документе.

:host(selector)

То же, что и :host, но применяется только в случае, если элемент-хозяин подходит под селектор selector.

Например, необходимо выровнять по центру <custom-dialog>, только если он содержит атрибут centered.

customElements.define('custom-dialog', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true)); } });

HTML:

<template id="tmpl"> <style> :host([centered]) { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); border-color: blue; } :host { display: inline-block; border: 1px solid red; padding: 10px; } </style> <slot></slot> </template> <custom-dialog centered>Centered!</custom-dialog> <custom-dialog>Not centered.</custom-dialog>

Применение стилей к содержимому слотов

Элементы слотов происходят из светлого DOM, поэтому они используют стили документа. Локальные стили не влияют на содержимое слотов.

Если нужно стилизовать слотовые элементы, то есть два варианта.

Первый – стилизовать сам <slot> и полагаться на наследование CSS:

this.shadowRoot.innerHTML = ` <style> slot[name="username"] { font-weight: bold; } </style> Имя: <slot name="username"></slot> `;

Другой вариант – использовать псевдокласс ::slotted(селектор). Выполняется если элемент соответствует селектору и это слотовый элемент, пришедший из светлого DOM. Имя слота не имеет значения. Может быть любой элемент, вставленный в <slot>, но только сам элемент, а не его потомки.

this.shadowRoot.innerHTML = ` <style> ::slotted(div) { border: 1px solid red; } </style> Name: <slot name="username"></slot> `;

Селектор ::slotted не может спускаться дальше в слот. Селекторы ::slotted(div span) или ::slotted(div) p недействительны.

Кроме того, ::slotted можно использовать только в CSS. Его нельзя использовать в querySelector.

CSS-хуки с пользовательскими свойствами

Пользовательские свойства CSS существуют одновременно на всех уровнях, как светлом, так и в тёмном DOM.

Например, в теневом DOM можно использовать CSS-переменную --user-card-field-color для стилизации полей, а документ будет её устанавливать.

customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.append(document.getElementById('tmpl').content.cloneNode(true)); } });

CSS:

user-card { --user-card-field-color: green; }

HTML:

<template id="tmpl"> <style> .field { color: var(--user-card-field-color, black); } </style> <div class="field">Имя: <slot name="username"></slot></div> <div class="field">Дата рождения: <slot name="birthday"></slot></div> </template> <user-card> <span slot="username">John Smith</span> <span slot="birthday">01.01.2001</span> </user-card>

Теневой DOM и события

События, которые произошли в теневом DOM, но пойманы снаружи этого DOM, имеют элемент-хозяин в качестве целевого элемента event.target.

Подмена целевого элемента не происходит, если событие берёт начало на элементе из слота, который фактически находится в обычном, светлом DOM.

Всплытие и метод event.composedPath()

event.composedPath() возвращает массив, отражающий цепочку родителей от целевого элемента до Window.

Детали теневого DOM-дерева доступны только для деревьев с {mode:'open'}. Если теневое DOM-дерево было создано с {mode: 'closed'}, то после композиции путь будет начинаться с элемента-хозяина.

Свойство: event.composed

Свойство composed объекта события регулирует возможность всплытия события сквозь границу теневого DOM. Если оно true, событие пересекает границу. Иначе, оно может быть поймано лишь внутри теневого DOM.

При генерации своего события для того, чтобы оно всплывало за пределы компонента, нужно установить свойства bubbles и composed в значение true.

Интерфейсные события

Основы событий мыши

Типы событий мыши

mousedown/mouseup - кнопка мыши нажата/отпущена над элементом.

mouseover/mouseout - курсор мыши появляется над элементом/уходит с него.

mousemove - каждое движение мыши над элементом.

dblclick - вызывается двойным кликом на элементе.

contextmenu - вызывается нажатием правой кнопки мыши.

Порядок событий

В случае, когда одно действие инициирует несколько событий, порядок их выполнения фиксирован. Обработчики событий вызываются в следующем порядке: mousedownmouseupclick.

Кнопки мыши

Обработчики mousedown и mouseup срабатывают на любую кнопку мыши, в отличии от click и contextmenu.

События, связанные с кликом, имеют свойство button, которое позволяет получить конкретную кнопку мыши.

event.button === 0 для левой кнопки мыши.

event.button === 1 для средней кнопки мыши.

event.button === 2 для правой кнопки мыши.

event.button === 3 для кнопки X1 (назад).

event.button === 4 для кнопки X2 (вперед).

Модификаторы: shift, alt, ctrl и meta

Все события мыши включают в себя информацию о нажатых клавишах-модификаторах.

shiftKey - Shift

altKey - Alt (или Opt для Mac)

ctrlKey - Ctrl

metaKey - Cmd для Mac

На практике, чтобы обработать Ctrl, нужно сделать следующую проверку:

if (event.ctrlKey || event.metaKey) // на Mac вместо Ctrl используется клавиша Cmd

Координаты: clientX/Y, pageX/Y

Все события мыши имеют координаты двух видов:

  1. Относительно окна: clientX и clientY.
  2. Относительно документа: pageX и pageY.

Отключаем выделение

В некоторых интерфейсах эффект выделения текста вследствие двойного клика мышью может быть неудобен. Если зажать левую кнопку мыши и, не отпуская кнопку, провести мышью, также возникнет выделение.

В данном случае для запрета выделения следует отменить действие браузера по умолчанию при событии mousedown.

Движение мыши: mouseover/out, mouseenter/leave

События mouseover/mouseout, relatedTarget

Событие mouseover происходит в момент, когда курсор оказывается над элементом, а событие mouseout – в момент, когда курсор уходит с элемента.

У этих событий имеется свойство relatedTarget.

Для события mouseover event.relatedTarget – это элемент, с которого курсор ушёл (relatedTargettarget).

Для события mouseout event.relatedTarget – это элемент, на который курсор перешёл (targetrelatedTarget).

Свойство relatedTarget может иметь значение null если указатель мыши пришел из-за пределов (вышел за пределы) окна браузера.

Пропуск элементов

Событие mousemove не генерируется при прохождении каждого пикселя мышью. Браузер проверяет позицию курсора периодически.

Если курсор мыши передвинуть очень быстро, события mouseover/mouseout для некоторых элементов под ним могут не произойти. Несмотря на то, что при быстрых переходах промежуточные элементы могут игнорироваться, элемент может быть пропущен только целиком. Если указатель «официально» зашёл на элемент, то есть было событие mouseover, то при выходе с него будет и событие mouseout.

Событие mouseout при переходе на потомка

Событие mouseout генерируется в том числе, когда указатель переходит с элемента на его потомка.

По логике браузера, курсор мыши может быть только над одним элементом в любой момент времени – над самым глубоко вложенным и верхним по z-index. Таким образом, если курсор переходит на другой элемент (пусть даже дочерний), он покидает предыдущий.

События mouseenter и mouseleave

События mouseenter/mouseleave похожи на mouseover/mouseout. Они тоже генерируются, когда курсор мыши переходит на элемент или покидает его, но имеют пару важных отличий:

  1. Переходы внутри элемента, на его потомки и с них, не считаются.
  2. События mouseenter/mouseleave не всплывают.

Делегирование событий

События mouseenter/leave просты в использовании. Но они не всплывают, а значит, их нельзя делегировать.

В случае, если элемент имеет другие вложенные элементы, делегирования достичь можно так:

  • Запоминать текущий элемент в переменную currentElem.
  • На mouseover – игнорировать событие, если мы всё ещё внутри currentElem.
  • На mouseout – игнорировать событие, если это не уход с currentElem.

Пример кода, учитывающего все ситуации:

let currentElem = null; table.onmouseover = function(event) { if (currentElem) return; let target = event.target.closest('td'); if (!target || !table.contains(target)) return; currentElem = target; target.style.background = 'pink'; }; table.onmouseout = function(event) { if (!currentElem) return; let relatedTarget = event.relatedTarget; while (relatedTarget) { if (relatedTarget === currentElem) return; relatedTarget = relatedTarget.parentNode; } currentElem.style.background = ''; currentElem = null; };

Drag'n'Drop с событиями мыши

События dragstart, dragend позволяют легко решать простые задачи. Но у них есть и ограничения. Например, нельзя организовать перенос только по горизонтали/вертикали, нельзя ограничить перенос внутри заданной зоны и тд.

Здесь будет рассмотрен Drag’n’Drop при помощи событий мыши.

Алгоритм Drag’n’Drop

  1. При mousedown – подготовить элемент к перемещению, если необходимо (например, создать его копию).
  2. Затем при mousemove передвинуть элемент на новые координаты путём смены left/top и position:absolute.
  3. При mouseup – остановить перенос элемента и произвести все действия, связанные с окончанием Drag’n’Drop.

Чтобы браузер не запускал автоматически свой собственный Drag’n’Drop, его нужно отключить:

elem.ondragstart = () => false;

Событие mousemove должно отслеживаться на document, чтобы из-за быстрого движения указатель не слетел с элемента (так как mousemove не генерирутся для каждого пикселя).

Правильное позиционирование

Чтобы изначальный сдвиг курсора относительно элемента сохранялся, при mousedown необходимо запомнить расстояние от курсора до левого верхнего угла переносимого элемента в переменных shiftX/shiftY.

let shiftX = event.clientX - elem.getBoundingClientRect().left; let shiftY = event.clientY - elem.getBoundingClientRect().top;

Далее, при переносе элемента он должен позиционироваться с тем же относительным сдвигом:

elem.style.left = event.pageX - shiftX + 'px'; elem.style.top = event.pageY - shiftY + 'px';

Цели переноса (droppable)

В реальности обычно берут один элемент и перетаскивают в другой. Абстрактно говоря, перетаскиваемый (draggable) помещают в «цель переноса» (droppable).

При перемещении перетаскиваемый элемент всегда находится поверх других элементов. А события мыши срабатывают только на верхнем элементе, но не на нижнем.

Метод document.elementFromPoint(clientX, clientY) возвращает наиболее глубоко вложенный элемент по заданным координатам окна (или null, если указанные координаты находятся за пределами окна).

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

elem.hidden = true; let elemBelow = document.elementFromPoint(event.clientX, event.clientY); elem.hidden = false;

Зная, над каким droppable произошло событие, цель переноса можно подсветить, обработать окончание переноса, когда оно случится и тд.

Клавиатура: keydown и keyup

Событие keydown происходит при нажатии клавиши, а keyup – при отпускании.

event.code и event.key

Свойство key объекта события позволяет получить символ, а свойство code – «физический код клавиши». Этот код не изменится при изменении языка или регистра, он зависит от расположения клавиши на клавиатуре.

  • Буквенные клавиши имеют коды по типу "Key<буква>": "KeyA", "KeyB" и тд
  • Коды числовых клавиш строятся по принципу: "Digit<число>": "Digit0", "Digit1" и тд
  • Код специальных клавиш – это их имя: "Enter", "Backspace", "Tab" и тд
document.addEventListener('keydown', function(event) { if (event.code == 'KeyZ' && (event.ctrlKey || event.metaKey)) { console.log('Отменить!') } });
Автоповтор

При долгом нажатии клавиши возникает автоповтор: keydown срабатывает снова и снова, и когда клавишу отпускают, то отрабатывает keyup.

Для событий, вызванных автоповтором, у объекта события свойство event.repeat равно true.

События указателя

События указателя позволяют одновременно обрабатывать действия с помощью мыши, касания и пера.

Типы событий указателя

Типы событий указателя

Можно заменить события mouse на аналогичные pointer с уверенностью, что с мышью по-прежнему всё будет работать нормально.

Свойства событий указателя

События указателя содержат те же свойства, что и события мыши и несколько дополнительных:

pointerId – уникальный идентификатор указателя, вызвавшего событие, генерируется браузером. Это свойство позволяет обрабатывать несколько указателей, например сенсорный экран со стилусом и мульти-тач.

pointerType'mouse'/'pen'/'touch' - тип указывающего устройства.

isPrimary – равно true для основного указателя (первый палец в мульти-тач).

Мульти-тач

Можно отслеживать несколько касающихся экрана пальцев, используя их pointerId. Когда пользователь перемещает, а затем убирает палец, получаем события pointermove и pointerup с тем же pointerId, что и при событии pointerdown.

Событие: pointercancel

Событие pointercancel происходит, когда текущее действие с указателем по какой-то причине прерывается, и события указателя больше не генерируются:

  • Указывающее устройство было физически выключено
  • Изменилась ориентация устройства (перевернули планшет)
  • Браузер решил сам обработать действие, считая его жестом мыши, масштабированием и тд

Чтобы предотвратить действие браузера по умолчанию и избежать pointercancel, нужно сделать две вещи:

  1. Предотвратить запуск встроенного drag’n’drop с помощью elem.ondragstart = () => false.
  2. Для устройств с сенсорным экраном существуют другие действия браузера, связанные с касаниями. Можно предотвратить их, добавив в CSS свойство elem { touch-action: none }.

Захват указателя

Метод elem.setPointerCapture(pointerId) – привязывает события с данным pointerId к elem. После такого вызова все события указателя с таким pointerId будут иметь elem в качестве целевого элемента (как будто произошли над elem), вне зависимости от того, где в документе они произошли.

Эта привязка отменяется:

  • автоматически, при возникновении события pointerup или pointercancel
  • автоматически, если elem удаляется из документа
  • при вызове elem.releasePointerCapture(pointerId)

Захват указателя используется для упрощения операций с переносом (drag’n’drop) элементов.

Благодаря setPointerCapture если в документе есть какие-то другие обработчики pointermove, они не будут нечаянно вызваны, пока пользователь находится в процессе перетаскивания элемента. Кроме этого, код станет чище, поскольку вместо обработчика mousemove на document можно будет использовать обработчик pointermove на elem. Удаление привязки происходит автоматически.

События при захвате указателя

gotpointercapture срабатывает, когда элемент использует setPointerCapture для включения захвата.

lostpointercapture срабатывает при освобождении от захвата: явно с помощью releasePointerCapture или автоматически, когда происходит событие pointerup/pointercancel.

Прокрутка

Событие прокрутки scroll позволяет реагировать на прокрутку страницы или элемента. Оно позволяет показать/скрыть дополнительные элементы управления или информацию, основываясь на том, в какой части документа находится пользователь, подгрузить данные, когда пользователь прокручивает страницу вниз до конца и тд.

Событие scroll работает как на window, так и на других элементах, на которых включена прокрутка.

Предотвращение прокрутки

Нельзя предотвратить прокрутку, используя event.preventDefault() в обработчике onscroll, потому что он срабатывает после того, как прокрутка уже произошла.

Но можно предотвратить прокрутку, используя event.preventDefault() на событии, которое вызывает прокрутку, например, keydown для клавиш pageUp и pageDown.

Способов инициировать прокрутку много, поэтому более надёжный способ – использовать CSS-свойство overflow.

Формы, элементы управления

Свойства и методы формы

Навигация: формы и элементы

document.forms содержит HTMLCollection всех форм документа. Позволяет получить форму как по номеру, так и по имени.

Любой элемент формы можно получить с помощью именованной коллекции form.elements. Если элементов с одним и тем же именем несколько, form.elements[name] вернет их коллекцию.

Элементы <fieldset> также поддерживают свойство elements.

Сокращённая форма записи form[index/name] также работает, однако, если получить элемент, а затем изменить его свойство name, он будет доступен и под старым, и под новым именем.

Обратная ссылка: element.form

Для любого элемента форма доступна через свойство form.

Элементы формы

Рассмотрим элементы управления, используемые в формах.

input и textarea

К их значению можно получить доступ через свойство value, например, input.value (строковое) или input.checked (логическое) для чекбоксов.

textarea.innerHTML/textContent хранят первоначальное значение. Для получения текущего необходимо использовать свойство value.

select и option

Основные свойства элемента <select>:

select.options – коллекция из подэлементов <option>.

select.value – значение выбранного в данный момент <option>.

select.selectedIndex – номер выбранного <option>.

Установить значение в <select> можно следующими способами:

Найти соответствующий элемент <option> и установить в option.selected значение true.

Установить в select.value значение нужного <option>.

Установить в select.selectedIndex номер нужного <option>.

В отличие от большинства других элементов управления, <select> позволяет выбрать несколько вариантов одновременно, если у него стоит атрибут multiple. В этом случае для работы со значениями необходимо использовать первый способ, то есть ставить или удалять свойство selected у подэлементов <option>.

new Option

option = new Option(text, value, defaultSelected, selected) - создание элемента <option>.

text – текст внутри <option>.

value – значение атрибута value.

defaultSelected – если true, ставится HTML-атрибут selected.

selected – если true, то элемент <option> будет выбранным.

Элементы <option> имеют свойства:

option.selected - выбрана ли опция.

option.index - номер опции среди других в списке <select>.

option.value - значение опции.

option.text - видимое содержимое опции.

Фокусировка: focus/blur

События focus/blur

Событие focus вызывается в момент фокусировки, а blur – когда элемент теряет фокус.

onblur срабатывает после потери фокуса элементом, поэтому отменить потерю фокуса через event.preventDefault() нельзя.

Методы focus/blur

Методы elem.focus() и elem.blur() устанавливают/снимают фокус.

Например, запретим посетителю переключаться с поля ввода, если введённое значение не прошло валидацию:

input.onblur = function() { if (!this.value.includes('@')) { this.classList.add("error"); input.focus(); } else { this.classList.remove("error"); } };

Потеря фокуса может произойти не только, когда посетитель кликает куда-то ещё. alert при появлении переводит фокус на себя, а когда alert закрывается – элемент получает фокус обратно. Или, если элемент удалить из DOM, фокус также будет потерян.

Включаем фокусировку на любом элементе: tabindex

Поддержка focus/blur гарантирована для элементов, с которыми посетитель может взаимодействовать: <button>, <input>, <select>, <a> и тд. С другой стороны, элементы <div>, <span>, <table> и тд – по умолчанию не могут получить фокус. Это можно изменить HTML-атрибутом tabindex.

Любой элемент поддерживает фокусировку, если имеет tabindex. Значение этого атрибута – порядковый номер элемента, который используется для переключения между элементами. Порядок перебора таков: сначала идут элементы со значениями tabindex от 1 и выше, а затем элементы без tabindex. Есть два специальных значения:

tabindex="0" ставит элемент в один ряд с элементами без tabindex. Используется, чтобы включить фокусировку на элементе, но не менять порядок переключения.

tabindex="-1" позволяет фокусироваться на элементе только программно. Клавиша Tab проигнорирует такой элемент, но метод elem.focus() будет действовать.

tabindex можно добавить из JavaScript, используя свойство elem.tabIndex. Это даст тот же эффект.

Текущий элемент с фокусом можно получить как document.activeElement.

События focusin/focusout

События focus и blur не всплывают, но передаются вниз на фазе перехвата.

События focusin и focusout – такие же, как и focus/blur, но они всплывают. Обработчик focusin/focusout можно добавить только через addEventListener.

События: change, input, cut, copy, paste

Событие: change

Событие change срабатывает по окончании изменения элемента. Для текстовых <input> это означает, что событие происходит при потере фокуса.

Событие: input

Событие input срабатывает каждый раз после изменения значения.

События: cut, copy, paste

Эти события происходят при вырезании/копировании/вставке данных, эти действия можно предотвратить, вызвав event.preventDefault().

Свойство event.clipboardData в обработчике paste позволяет получить доступ к буферу обмена.

input.onpaste = function(event) { console.log(event.clipboardData.getData('text/plain')); // возвращает вставляемый текст return false; };

Метод document.getSelection() в обработчиках cut и copy вернет текст, который пользователь копирует/вырезает.

input.oncut = input.oncopy = function(event) { console.log(event.type + ' - ' + document.getSelection()); return false; };

Отправка формы: событие и метод submit

Событие: submit

Есть два основных способа отправить форму:

  1. Нажать кнопку <input type="submit"> или <input type="image">.
  2. Нажать Enter, находясь на каком-нибудь поле.

Оба действия сгенерируют событие submit на форме. Обработчик может проверить данные, и в случае чего вызвать event.preventDefault() чтобы форма не была отправлена на сервер.

Взаимосвязь между submit и click

При отправке формы по нажатию Enter в текстовом поле, генерируется событие click на кнопке <input type="submit">.

Метод: submit

Чтобы отправить форму на сервер вручную, нужно вызвать метод form.submit(). При этом событие submit не генерируется.

Загрузка документа и ресурсов

Страница: DOMContentLoaded, load, beforeunload, unload

DOMContentLoaded

Событие DOMContentLoaded срабатывает на объекте document. Обработчик необходимо добавлять с помощью addEventListener.

DOMContentLoaded ждет загрузки документа и скриптов, за исключением скриптов с атрибутом async и сгенерированных динамически как document.createElement('script'). Изображения и другие ресурсы всё ещё могут продолжать загружаться.

Внешние таблицы стилей не затрагивают DOM, поэтому DOMContentLoaded их не ждёт. Однако, если после стилей есть скрипт, этот скрипт должен дождаться, пока загрузятся стили. Так как DOMContentLoaded дожидается скриптов, то он также дожидается и стилей перед ними.

window.onload

Событие load на объекте window наступает, когда загрузилась вся страница, включая стили, картинки и другие ресурсы.

window.onbeforeunload

Событие beforeunload на window генерируется, когда пользователь покидает страницу. Если отменить событие, браузер спросит, на самом ли деле пользователь хочет уйти.

window.onbeforeunload = () => false;

window.onunload

Событие unload на window генерируется, когда пользователь окончательно уходит.

В обработчике можно совершать простые действия, не требующие много времени, вроде закрытия связанных всплывающих окон. Обычно его используют для отправки статистики. С помощью метода navigator.sendBeacon(url, data) можно послать POST-запрос (размер данных ограничен 64 Кб), не задерживая переход к другой странице.

В методе fetch для таких запросов с закрывающейся страницей есть специальный флаг keepalive.

readyState

Свойство document.readyState показывает текущее состояние загрузки.

Есть три возможных значения:

  • "loading" – документ загружается
  • "interactive" – документ был полностью прочитан
  • "complete" – документ был полностью прочитан и все ресурсы (такие как изображения) были тоже загружены

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

if (document.readyState == 'loading') { document.addEventListener('DOMContentLoaded', work); } else { work(); }
readystatechange

Событие readystatechange – альтернативный вариант отслеживания состояния загрузки документа. Используется редко.

document.onreadystatechange = () => console.log(document.readyState);

Событие readystatechange генерируется прямо перед DOMContentLoaded с document.readyState === 'interactive' и прямо перед window.onload с document.readyState === 'complete'.

Скрипты: async, defer

Когда браузер загружает HTML и доходит до тега <script>...</script>, он не может продолжать строить DOM пока не выполнит скрипт. Из-за этого скрипты не видят DOM-элементы ниже себя, а если вверху страницы объёмный скрипт, он «блокирует» страницу.

defer

Атрибут defer сообщает браузеру, что он должен продолжать обрабатывать страницу и загружать скрипт в фоновом режиме, а затем запустить этот скрипт, когда DOM дерево будет полностью построено, но до события DOMContentLoaded.

Скрипты с defer никогда не блокируют страницу.

Отложенные с помощью defer скрипты могут загружаться параллельно, но выполняются в том же порядке, в котором объявлены в документе.

Атрибут defer предназначен только для внешних скриптов. Он будет проигнорирован, если в теге <script> нет src.

async

Атрибут async означает, что скрипт абсолютно независим. Скрипт с async не ждет другие скрипты, так же как остальные скрипты не ждут скрипта с async.

DOMContentLoaded может произойти как до асинхронного скрипта, так и после.

Если есть несколько скриптов с async, они могут выполняться в любом порядке. Чем раньше скрипт загрузится, тем раньше он будет выполнен.

Асинхронные скрипты полезны для добавления на страницу сторонних скриптов: счётчиков, рекламы и тд. Они не зависят от наших скриптов, и мы тоже не должны их ждать.

Динамически загружаемые скрипты

Динамически загружаемые скрипты по умолчанию ведут себя как async-скрипты.

Это поведение можно отменить, установив свойство async в false. С этим флагом порядок выполнения будет зависеть от расположения скриптов в документе (однако, такие скрипты выполнятся позже тех, что изначально были в html).

let script = document.createElement('script'); script.src = "/article.js"; script.async = false; document.body.append(script);

Загрузка ресурсов: onload и onerror

Браузер позволяет отслеживать загрузку сторонних ресурсов: скриптов, ифреймов, изображений и тд. События:

load – загрузка завершена успешно.

error – во время загрузки произошла ошибка.

Загрузка скриптов

Событие load срабатывает после того, как скрипт был загружен и выполнен. В обработчике onload можно использовать переменные, вызывать функции и тд, которые предоставляет сторонний скрипт.

Ошибки, возникающие во время загрузки скрипта, могут быть отслежены с помощью события error.

script.onerror = function() { alert("Ошибка загрузки " + this.src); };

Обработчики onload/onerror отслеживают только сам процесс загрузки. Чтобы поймать ошибки в скрипте, нужно воспользоваться глобальным обработчиком window.onerror.

Другие ресурсы

События load и error срабатывают для любых ресурсов, у которых есть внешний src.

Большинство ресурсов начинают загружаться после их добавления в документ. За исключением тега <img>. Изображения начинают загружаться, когда получают src.

Для <iframe> событие load срабатывает по окончании загрузки как в случае успеха, так и в случае ошибки.

Ошибка в скрипте с другого источника

Один источник (домен/порт/протокол) не может получить доступ к содержимому с другого источника.

Если используется скрипт с другого домена, и в нем имеется ошибка, информация об этой ошибке будет спрятана.

Чтобы разрешить кросс-доменный доступ, нужно поставить тегу <script> атрибут crossorigin, и, кроме того, удалённый сервер должен поставить специальные заголовки. Похожая кросс-доменная политика (CORS) внедрена и в отношении других ресурсов.

Существует три уровня кросс-доменного доступа:

  1. Атрибут crossorigin отсутствует – доступ запрещён.
  2. crossorigin="anonymous" – доступ разрешён, если сервер отвечает с заголовком Access-Control-Allow-Origin со значениями * или наш домен. Браузер не отправляет авторизационную информацию и куки на удалённый сервер.
  3. crossorigin="use-credentials" – доступ разрешён, если сервер отвечает с заголовками Access-Control-Allow-Origin со значением наш домен и Access-Control-Allow-Credentials: true. Браузер отправляет авторизационную информацию и куки на удалённый сервер.

При условии, что сервер предоставил заголовок Access-Control-Allow-Origin, у нас будет полный отчёт по ошибкам:

<script> window.onerror = function(message, url, line, col, errorObj) { console.log(`${message}\n${url}, ${line}:${col}`); // Uncaught ReferenceError: noSuchFunction is not defined }; </script> <script crossorigin="anonymous" src="https://cors.javascript.info/article/onload-onerror/crossorigin/error.js"></script>

Разное

MutationObserver: наблюдатель за изменениями

MutationObserver – это встроенный объект, наблюдающий за DOM-элементом и запускающий колбэк в случае изменений.

Синтаксис

let observer = new MutationObserver(callback) - создание наблюдателя за изменениями.

observer.observe(node, config) - прикрепление к DOM-узлу node с параметрами config, описывающими, на какие изменения реагировать. Свойства объекта config имеют логические значения:

childList – изменения в непосредственных детях node.

subtree – во всех потомках node.

attributes – в атрибутах node.

attributeFilter – массив имён атрибутов, чтобы наблюдать только за выбранными.

characterData – наблюдать ли за node.data (текстовым содержимым).

characterDataOldValue – если true, будет передавать и старое, и новое значение node.data в callback, иначе только новое (также требуется опция characterData).

attributeOldValue – если true, будет передавать и старое, и новое значение атрибута в callback, иначе только новое (также требуется опция attributes).

После изменений в node, выполняется callback, в который изменения передаются первым аргументом как список объектов MutationRecord, а сам наблюдатель идёт вторым аргументом.

Свойства MutationRecord

type – тип изменения, один из:

  • "attributes" изменён атрибут
  • "characterData" изменены данные elem.data, для текстовых узлов
  • "childList" добавлены/удалены дочерние элементы

target – где произошло изменение: элемент для "attributes" или "childList", текстовый узел для "characterData".

addedNodes/removedNodes – добавленные/удалённые узлы.

previousSibling/nextSibling – предыдущий или следующий одноуровневый элемент для добавленных/удалённых элементов.

attributeName/attributeNamespace – имя/пространство имён (для XML) изменённого атрибута.

oldValue – предыдущее значение, только для изменений атрибута или текста, если включена соответствующая опция attributeOldValue/characterDataOldValue.

Использование для интеграции

Представим ситуацию, когда подключается сторонний скрипт, который добавляет полезную функциональность на страницу, но при этом делает что-то лишнее, например, показывает рекламу <div class="ads">Ненужная реклама</div>.

Используя MutationObserver, можно отследить, когда в DOM появится такой элемент и удалить его. А полезную функциональность оставить.

Использование для архитектуры

Есть и ситуации, когда MutationObserver хорошо подходит с архитектурной точки зрения.

Например, на сайте о программировании используется JavaScript-библиотека для подсветки синтаксиса Prism.js. Вызов метода Prism.highlightElem(elem) добавляет в элементы elem с примерами кода стили и теги, которые в итоге дают цветную подсветку синтаксиса.

Но в случае динамической подгрузки новых статей может быть не очень удобно искать elem в них и вызывать для них Prism.highlightElem.

Можно использовать MutationObserver, чтобы автоматически определять момент, когда примеры кода появляются на странице, и подсвечивать их.

Дополнительные методы

observer.disconnect() – останавливает наблюдение за узлом.

mutationRecords = observer.takeRecords() – получает список необработанных записей изменений, которые произошли, но колбэк для них ещё не выполнился.

Сборка мусора

Объекты MutationObserver используют внутри себя так называемые «слабые ссылки» на узлы, за которыми смотрят. Если узел удалён из DOM и больше не достижим, то он будет удалён из памяти вне зависимости от наличия наблюдателя.

Intersection Observer API

IntersectionObserver – объект, предоставляющий способ асинхронного наблюдения за изменениями в пересечении элемента с предком или с viewport.

Синтаксис

let observer = new IntersectionObserver(callback, options) - создание наблюдателя за пересечениями.

observer.observe(target) - прикрепление к элементу target.

observer.unobserve(target) - открепление от элемента target.

observer.disconnect() - отключение наблюдателя от всех элементов.

options

Объект options содержит параметры observer. Основные из них:

  • root – элемент, который используется в качестве окна просмотра для проверки видимости целевого объекта target, должен быть предком target. Если root не задан или равен null, используется окно просмотра браузера (viewport)
  • threshold – степень пересечения target и root. Задаётся в диапазоне 0 <= n <= 1, где 1 означает, что 100% элемента target находится в пределах root. Можно задать массив значений, при которых будет запускаться callback. Значение по умолчанию: 0
  • rootMargin – отступ вокруг элемента root. Строка от одного до четырех значений, аналогичных CSS-свойству margin. Значения могут быть указаны только в пикселях или процентах. Используется для увеличения или уменьшения (если значение отрицательное) сторон элемента root перед вычислением пересечений. Значение по умолчанию: "0px 0px 0px 0px"
callback

function callback(entries, observer) {} – функция, которая вызывается каждый раз при изменении элементом target статуса пересечения.

entries – массив объектов со свойствами:

  • time – число миллисекунд DOMHightResTimeStamp, указывающее, когда произошло пересечение
  • rootBounds – объект DOMRectReadOnly (имеет свойства x, y, width, height, top, left, bottom, right) элемента root
  • boundingClientRect – объект DOMRectReadOnly элемента target
  • intersectionRect – объект DOMRectReadOnly области пересечения target с root
  • intersectionRatio – число от 0 до 1 включительно, указывающее, какая доля площади target видна в пределах области пересечения
  • target – сам элемент, к которому прикреплен observer
  • isIntersectingtrue, если target пересекается с root, иначе false

observer – сам наблюдатель за пересечениями.

Пример
const imageObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.src = entry.target.dataset.src; observer.unobserve(entry.target); } }); }, { rootMargin: "50px 0px" }); document.querySelectorAll('img') .forEach(img => imageObserver.observe(img));

Selection и Range

Range

Range представляет собой пару граничных точек: начало и конец диапазона.

Каждая точка представлена как родительский DOM-узел с относительным смещением от начала. Для узла-элемента смещение – номер дочернего элемента, для текстового узла – позиция в тексте.

let range = new Range() - создание диапазона.

range.setStart(node, offset)/range.setEnd(node, offset) - устанавливает начальную/конечную границу выделения на позицию offset в node.

document.getSelection().addRange(range) - применяет выделение. Если выделение уже существует, сначала его нужно снять, используя document.getSelection().removeAllRanges(). В противном случае все браузеры, кроме Firefox, проигнорируют добавление.

HTML: <p id="p">Example: <i>italic</i> and <b>bold</b></p>

let range = new Range(); range.setStart(p.firstChild, 2); range.setEnd(p, 2); document.getSelection().addRange(range);

Диапазон может охватывать множество не связанных между собой элементов. Важно лишь чтобы конец шёл после начала.

Свойства объекта диапазона Range

startContainer/endContainer - узел node, в который установлена начальная/конечная граница выделения.

startOffset/endOffset – позиция offset начального/конечного смещения.

collapsedtrue, если диапазон начинается и заканчивается на одном и том же месте, иначе false.

commonAncestorContainer – ближайший общий предок всех узлов в пределах диапазона.

Другие методы Range

setStartBefore(node)/setEndBefore(node) - устанавливает начальную/конечную границу выделения прямо перед node.

setStartAfter(node)/setEndAfter(node) - устанавливает начальную/конечную границу выделения прямо после node.

selectNode(node) - выделяет node целиком.

selectNodeContents(node) - выделяет всё содержимое node.

collapse(toStart) - схлопывает диапазон. Если toStart=true, конечная граница переносится в начало, иначе наоборот.

cloneRange() - создает новый диапазон с идентичными границами.

Методы для манипуляции содержимым в пределах диапазона:

deleteContents() – удаляет содержимое диапазона из документа.

extractContents() – удаляет содержимое диапазона из документа и возвращает его как DocumentFragment.

cloneContents() – клонирует содержимое диапазона и возвращает его как DocumentFragment.

insertNode(node) – вставляет node в документ перед началом диапазона.

surroundContents(node) – оборачивает node вокруг содержимого диапазона. Чтобы метод сработал, диапазон должен содержать как открывающие, так и закрывающие теги для всех элементов внутри себя.

Selection

Выделение в документе представлено объектом Selection, который может быть получен как window.getSelection() или document.getSelection().

Выделение может включать ноль или более диапазонов. На практике выделить несколько диапазонов в документе можно только в Firefox, используя Ctrl+click (Cmd+click для Mac). Остальные браузеры поддерживают максимум 1 диапазон.

Свойства Selection

anchorNode/focusNode – узел, с которого начинается/заканчивается выделение, null если выделения нет.

anchorOffset/focusOffset – смещение выделения в anchorNode/focusNode, 0 если выделения нет.

isCollapsedtrue, если диапазон выделения пуст или не существует, иначе false.

rangeCount – количество диапазонов в выделении, максимум 1 во всех браузерах, кроме Firefox.

Конец выделения может быть в документе до его начала.

События при выделении

elem.onselectstart – происходит когда с elem начинается выделение. Начало выделения можно отменить с помощью preventDefault().

document.onselectionchange – происходит каждый раз при изменении выделения. Вместе с методом document.getSelection() позволяет отслеживать текущее выделение.

Методы Selection

getRangeAt(i) – берет i-ый диапазон, начиная с 0. Во всех браузерах, кроме Firefox, используется только 0.

addRange(range) – добавляет range в выделение. Все браузеры, кроме Firefox, проигнорируют этот вызов, если в выделении уже есть диапазон.

removeRange(range) – удаляет range из выделения.

removeAllRanges()/empty() – удаляет все диапазоны.

Методы управления диапазонами выделения напрямую, без обращения к Range:

collapse(node, offset)/setPosition(node, offset) – заменяет выделенный диапазон новым, который начинается и заканчивается на node, на позиции offset.

collapseToStart()/collapseToEnd() – схлопывает (заменяет на пустой диапазон) к началу/концу выделения.

extend(node, offset) – изменяет конец выделения, перемещая focusNode в node и focusOffset в offset.

setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset) – заменяет диапазон выделения на заданный.

selectAllChildren(node) – выделяет все дочерние узлы node.

deleteFromDocument() – удаляет содержимое выделения из документа.

containsNode(node, allowPartialContainment = false) – проверяет, содержит ли выделение node (или его часть, если второй аргумент равен true).

Выделение в элементах форм

Элементы форм, такие как input и textarea, предоставляют отдельное API для выделения, работающее с текстом.

Событие onselect срабатывает, когда выделение завершено.

Свойства

selectionStart/selectionEnd – позиция начала/конца выделения, свойства можно изменять.

selectionDirection – направление выделения ('forward'/'backward'/'none').

Методы

select() – выделяет всё содержимое элемента формы.

setSelectionRange(start, end, [direction]) – изменяет selectionStart на start, selectionEnd на end и необязательный selectionDirection на direction.

setRangeText(replacement, [start], [end], [selectionMode]) – заменяет текст в диапазоне от selectionStart до selectionEnd на replacement. Аргументы start и end изменяют границы диапазона. selectionMode определяет, как будет вести себя выделение после замены текста. Значения:

  • "select" – только что вставленный текст будет выделен
  • "start" – диапазон выделения схлопнется прямо перед вставленным текстом (так что курсор окажется непосредственно перед ним)
  • "end" – диапазон выделения схлопнется прямо после вставленного текста (курсор окажется сразу после него)
  • ["preserve"] – пытается сохранить выделение

Сделать что-то невыделяемым

Существуют три способа:

  1. CSS-свойство user-select: none не позволяет начать выделение с elem (для iOS нужен префикс -webkit-), но пользователь может начать выделять с другого места и включить elem.
  2. Предотвратить действие по умолчанию в событии onselectstart (не поддерживается в Safari на iOS) или mousedown.
  3. Очистить выделение после срабатывания с помощью document.getSelection().empty(). Используется редко, так как вызывает нежелаемое мерцание при появлении и исчезновении выделения.

Page Visibility API

Свойства

document.visibilityState – возвращает текущее состояние видимости документа. Значения:

  • 'visible' – страница видна пользователю
  • 'hidden' – страница не видна (открыта другая вкладка, браузер свернут и тд)
  • 'prerender' – содержимое страницы проходит предварительную обработку и не видно пользователю
  • 'unloaded' – страница находится в процессе удаления из памяти

document.hidden – упрощенный вариант, поддерживается для совместимости. Значения:

  • true – эквивалент visibilityState === "hidden"
  • false – эквивалент visibilityState !== "hidden"

visibilitychange

Событие visibilitychange срабатывает на объекте document при изменении видимости страницы.

document.addEventListener("visibilitychange", function() { if (document.hidden) { console.log('Вкладка не активна'); } else { console.log('Вкладка активна'); } });

Событийный цикл: микрозадачи и макрозадачи

Событийный цикл

Событийный цикл – это бесконечный цикл, в котором движок JavaScript ожидает задачи, исполняет их и снова ожидает появления новых.

Примеры задач:

  • Когда загружается внешний скрипт <script src="...">, то задача – это выполнение этого скрипта
  • Когда пользователь двигает мышь, задача – сгенерировать событие mousemove и выполнить его обработчики
  • Когда истечёт таймер, установленный с помощью setTimeout(func, ...), задача – это выполнение функции func

Если поступает новая задача, а движок занят чем-то другим, она ставится в очередь макрозадач (macrotask queue). Задачи из очереди исполняются по принципу FIFO (first in, first out).

Рендеринг (отрисовка страницы) никогда не происходит во время выполнения задачи движком. Не имеет значения, сколь долго выполняется задача, изменения в DOM отрисовываются только после ее выполнения.

Макрозадачи и Микрозадачи

Микрозадачи приходят только из кода. Обычно они создаются промисами.

Также есть специальная функция queueMicrotask(func), которая помещает func в очередь микрозадач.

Сразу после каждой макрозадачи движок исполняет все задачи из очереди микрозадач до обработки каких-либо событий или рендеринга, или перехода к другой макрозадаче.

Событийный цикл

Используя setTimeout(f) с нулевой задержкой, можно разбить большую вычислительную задачу на части, чтобы браузер мог реагировать на пользовательские события и показывать прогресс выполнения этой задачи.

Web Workers

Для длительных тяжёлых вычислений, которые не должны блокировать событийный цикл, можно использовать Web Workers. Это способ исполнить код в другом, параллельном потоке.

Web Workers могут обмениваться сообщениями с основным процессом, но они имеют свои переменные и свой событийный цикл.

Web Workers не имеют доступа к DOM, поэтому основное их применение – вычисления. Они позволяют задействовать несколько ядер процессора одновременно.

LocalStorage, sessionStorage

Объекты веб-хранилища localStorage и sessionStorage позволяют хранить пары ключ/значение в браузере.

Данные, которые в них записаны, сохраняются после обновления страницы, а при использовани localStorage - даже после перезапуска браузера.

Большинство браузеров могут хранить до 5 мегабайт данных. Они не имеют срока давности, по которому истекают и удаляются.

Хранилище привязано к источнику (домен/протокол/порт). Разные протоколы или поддомены определяют разные объекты хранилища, и они не могут получить доступ к данным друг друга.

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

API

localStorage и sessionStorage предоставляют одинаковые свойства и методы:

  • setItem(key, value) – сохраняет значение value под ключом key
  • getItem(key) – возвращает значение по ключу key, либо null если ключ не существует
  • removeItem(key) – удаляет значение по ключу key
  • clear() – очищает объект хранилища
  • key(index) – возвращает имя ключа на позиции index
  • length – возвращает количество элементов в хранилище

SessionStorage

Используется реже, чем localStorage, так как имеет свои ограничения:

  1. Существует только в рамках текущей вкладки браузера. Другая вкладка с той же страницей будет иметь другое хранилище. Но оно разделяется между ифреймами на той же вкладке (если они из одного и того же источника).
  2. Данные продолжают существовать после перезагрузки страницы, но не после закрытия/открытия вкладки.

Событие storage

Событие storage генерируется при обновлении данных в localStorage или sessionStorage.

Событие срабатывает на всех остальных объектах window, где доступно хранилище, кроме того окна, которое его вызвало. Это позволяет разным окнам одного источника обмениваться сообщениями.

Объект события event имеет свойства, содержащие все данные о произошедшем обновлении (key, oldValue, newValue, url) и объект хранилища storageArea:

window.addEventListener('storage', function(e) { console.log(e); }); localStorage.setItem('test', '111');

Для связи между окнами одного источника современные браузеры также поддерживают более полнофункциональный Broadcast channel API.

Анимации

События transitionend/animationend

Событие transitionend срабатывает, когда CSS transition оканчивает своё выполнение.

Объект события содержит ряд полезных свойств:

  • propertyName - имя свойства, анимация которого завершилась. Может быть полезным при анимации нескольких свойств.
  • elapsedTime - время в секундах, которое заняла анимация без учёта transition-delay.

Событие animationend срабатывает, когда CSS animation (используется с @keyframes) оканчивает своё выполнение.

JavaScript анимации

Используются когда CSS не справляется или нужен жёсткий контроль над анимацией. JavaScript-анимации должны быть сделаны с помощью requestAnimationFrame. Использование:

let requestId = requestAnimationFrame(callback) - callback имеет один аргумент – время с начала загрузки страницы в миллисекундах. Это значение также может быть получено с помощью вызова performance.now().

cancelAnimationFrame(requestId) - отмена запланированного запуска callback.

Вспомогательная функция animate для создания анимации:

function animate({timing, draw, duration}) { let start = performance.now(); requestAnimationFrame(function animate(time) { let timeFraction = (time - start) / duration; // изменяется от 0 до 1 if (timeFraction > 1) timeFraction = 1; let progress = timing(timeFraction); // вычисление текущего состояния анимации draw(progress); // отрисовать её if (timeFraction < 1) { requestAnimationFrame(animate); } }); }

duration – общая продолжительность анимации в миллисекундах.

timing – функция вычисления прогресса анимации. Получает момент времени от 0 до 1, возвращает прогресс анимации, обычно тоже от 0 до 1.

draw – функция отрисовки анимации.