Что такое Virtual DOM?
За последний год я много слышал о Virtual DOM и React JS.
React работает действительно быстро и очень прост, но как он работает? Что такое Virtual DOM? Почему я должен беспокоиться об этом, и что случилось со старым добрым обычным DOM?
Что такое DOM
Перед тем, как мы начнем вникать в то, что из себя представляет DOM виртуальный, давайте немного поговорим о том, чем является DOM реальный.
DOM (аббревиатура от Document Object Model) — способ представления структурного документа с помощью объектов. Это кроссплатформенное и языко-независимое соглашение для представления и взаимодействия с данными в HTML, XML и т.д.
Веб-браузеры обрабатывают составляющие DOM, и мы можем взаимодействовать с ними, используя JavaScript и CSS. Мы можем работать с узлами документа, изменять их данные, удалять и вставлять новые узлы. В наши дни DOM API является практически кроссплатформенным и кроссбраузерным.
Так в чем же проблема?
Проблема DOM
Главная проблема DOM — он никогда не был рассчитан для создания динамического пользовательского интерфейса (UI). Мы можем работать с ним, используя JavaScript и библиотеки наподобие jQuery, но их использование не решает проблем с производительностью.
Посмотрите на современные социальные сети, такие как Twitter, Facebook или Pinterest.
После небольшого скроллинга, мы будем иметь десятки тысяч DOM-узлов, эффективно взаимодействовать с которыми — задача не из легких.
Для примера, попробуйте переместить 1000 div-блоков на 5 пикселей влево.
Это может занять больше секунды — это слишком много для современного интернета. Вы можете оптимизировать скрипт и использовать некоторые приемы, но в итоге это вызовет лишь головную боль при работе с огромными страницами и динамическим UI.
Можем ли мы решить эту проблему? Похоже, что можем.
В настоящее время W3C работает над новым стандартом Shadow DOM.
Shadow DOM — это рабочий черновик стандарта W3C. Спецификация, описывающая метод объединения нескольких DOM-деревьев в одну иерархию и как эти деревья взаимодействуют друг с другом в пределах документа, что позволяет лучше скомпоновать DOM.
Другой вариант заключается в использовании подхода с Virtual DOM.
Virtual DOM не является стандартом и в конечном итоге мы по-прежнему взаимодействуем с DOM, но делаем это как можно реже и более эффективно.
Virtual DOM
Вместо того, чтобы взаимодействовать с DOM напрямую, мы работаем с его легковесной копией. Мы можем вносить изменения в копию, исходя из наших потребностей, а после этого применять изменения к реальному DOM.
При этом происходит сравнение DOM-дерева с его виртуальной копией, определяется разница и запускается перерисовка того, что было изменено.
Такой подход работает быстрее, потому как не включает в себя все тяжеловесные части реального DOM.
Но только если мы делаем это правильно. Есть две проблемы: когда именно делать повторную перерисовку DOM и как это сделать эффективно.
- Первый из них — «dirty checking» (грязная проверка) заключается в том, чтобы опрашивать данные через регулярные промежутки времени и рекурсивно проверять все значения в структуре данных.
- Второй вариант — «observable» (наблюдаемый) заключается в наблюдении за изменением состояния. Если ничего не изменилось, мы ничего не делаем. Если изменилось, мы точно знаем, что нужно обновить.
- Эффективные алгоритмы сравнения
- Группировка операций чтения/записи при работе с DOM
- Эффективное обновление только под-деревьев
React JS
React JS — это JavaScript-библиотека, разработанная в Facebook для создания пользовательских интерфейсов, которая популяризировала идею использования виртуального DOM. React создает легковесное дерево из JavaScript-объектов для имитации DOM-дерева. Затем он создает из них HTML, который вставляется или добавляется к нужному DOM-элементу, что вызывает перерисовку страницы в браузере.
React — это библиотека, а не фреймворк, поэтому сравнивать его с Angular или Ember некорректно.
Другие библиотеки и фреймворки
- virtual-dom — реализация Virtual DOM от Matt Esch, алгоритм сравнения отличий.
- Mithril — Javascript-фреймворк для создания ярких приложений.
- Bobril — Компонент-ориентированный фреймворк, вдохновленный подходами Mithril и ReactJs.
- cito.js — JavaScript-фреймворк для создания быстрых, масштабируемых и модульных веб-приложений.
Вывод
Virtual DOM — это техника и набор библиотек / алгоритмов, которые позволяют нам улучшить производительность на клиентской стороне, избегая прямой работы с DOM путем работы с легким JavaScript-объектом, имитирующем DOM-дерево.
Идея с использованием виртуального DOM отличная, хотя и не нова — мы давно знали, что прямая работа с DOM обходится дорого. Используя библиотеки наподобие React, мы можем повысить производительность приложений и сделать это очень просто.
От переводчика
Оригинальная статья: What is Virtual DOM
Примечание: при переводе допущены некоторые вольности, но в рамках разумного.
Virtual Dom — JS: React
В одном из предыдущих курсов впервые рассматривалось изменение DOM в процессе взаимодействия со страницей. Этот способ резко отличается от того, который использовался в курсе JS: DOM API. Важнейшее отличие связано с тем, как происходит изменение состояния отрисованного экрана. При прямом манипулировании DOM нужно сделать следующее:
- Удалить из DOM то, что стало неактуально для следующего состояния.
- Изменить, если надо, те элементы, которые присутствуют на экране и должны остаться в новом.
- Добавить новые элементы на страницу (точечно).
Другими словами, чтобы перейти в новое состояние, нужно изменить старое. Значит про него надо знать.
В React всё совсем по-другому. После любого изменения и вызова setState React создаёт новое состояние и отрисовывает все компоненты так, как будто это происходит с нуля. На самом деле отрисовка действительно происходит с нуля. Неважно, что было до этого момента на экране и как оно располагалось. Любое изменение в React приводит к тому, что приложение отрисовывается заново.
Создатели React называют этот подход one-way data flow:
- Действия пользователя приводят к изменению состояния приложения (через setState ).
- React запускает цикл отрисовки. Начиная от того компонента, в котором было изменено состояние (как правило, корневой компонент), через пропсы данные постепенно распространяются от компонентов более высокого уровня до самых глубинных компонентов.
- Получившийся html интегрируется в страницу.
Те, кто хорошо знаком с функциональным подходом, могут увидеть прямую связь. React действительно делает мир неизменяемым (immutable). Самый простой способ реализовать подобное поведение — использовать mountElement.innerHTML , который заменяет html целиком после вызова setState . Хотя на практике этот подход сопряжён с кучей сложностей (пример реализации смотрите в дополнительных материалах), он позволяет в 200 строк построить библиотеку, которая будет работать как React.
Главная проблема при использовании innerHTML связана с производительностью. Сказать, что это медленно — ничего не сказать. Поэтому создатели React пошли другим путём.
Дерево виртуального DOM
Ранее в курсе говорилось, что компоненты отрисовываются, и это немного не так. В реальности после того, как отработает их рендеринг (вызов функции render для всего дерева компонентов), создаётся так называемый виртуальный DOM (virtual DOM). Это просто JS-объект определённой структуры, который отражает состояние экрана. Далее React сравнивает новое дерево виртуального DOM со старым и строит разницу между ними («дифф», или объект, описывающий разницу между старым и новым состоянием). И только в этот момент начинается отрисовка нового состояния в реальный DOM. Здесь уже должно быть понятно, что React умнее, чем кажется на первый взгляд, и вносит изменения в реальный DOM настолько эффективно, насколько это возможно, ведь он знает КАК его надо изменить.
Из описанного выше есть важное следствие. Тот реальный DOM, который находится под контролем React (это все потомки элемента, в который рендерится корневой компонент), не может изменяться никем снаружи React-приложения. Если подобное произойдёт, то React не сможет нормально функционировать, ведь ему приходится отслеживать текущее состояние DOM для того, чтобы производить вычисления «диффа». Когда подобное происходит, React ругается и говорит, что ему мешают работать.
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Виртуальный DOM и детали его реализации в React
Виртуальный DOM (VDOM) — это концепция программирования, в которой идеальное или «виртуальное» представление пользовательского интерфейса хранится в памяти и синхронизируется с «настоящим» DOM при помощи библиотеки, такой как ReactDOM. Этот процесс называется согласованием.
Такой подход и делает API React декларативным: вы указываете, в каком состоянии должен находиться пользовательский интерфейс, а React добивается, чтобы DOM соответствовал этому состоянию. Это абстрагирует манипуляции с атрибутами, обработку событий и ручное обновление DOM, которые в противном случае пришлось бы использовать при разработке приложения.
Поскольку «виртуальный DOM» — это скорее паттерн, чем конкретная технология, этим термином иногда обозначают разные понятия. В мире React «виртуальный DOM» обычно ассоциируется с React-элементами , поскольку они являются объектами, представляющими пользовательский интерфейс. Тем не менее, React также использует внутренние объекты, называемые «волокнами» (fibers), чтобы хранить дополнительную информацию о дереве компонентов. Их также можно считать частью реализации «виртуального DOM» в React.
Теневой DOM похож на виртуальный DOM?
Нет, они совсем разные. Теневой DOM (Shadow DOM) — это браузерная технология, предназначенная в основном для определения области видимости переменных и CSS в веб-компонентах. Виртуальный DOM — это концепция, реализованная библиотеками в JavaScript поверх API браузера.
Что такое «React Fiber»?
Fiber — новый механизм согласования в React 16, основная цель которого сделать рендеринг виртуального DOM инкрементным. Узнать больше об этом.
Немного о том, как работает виртуальный DOM в React
DOM расшифровывается как Document Object Model (объектная модель документа). Проще говоря, DOM — это представление пользовательского интерфейса (user interface, UI) в приложении. При каждом изменении UI, DOM также обновляется для отображения этих изменений. Частые манипуляции с DOM негативно влияют на производительность.
Что делает манипуляции с DOM медленными?
DOM представляет собой древовидную структуру данных. Поэтому изменения и обновления самого DOM являются достаточно быстрыми. Но после изменения обновленный элемент и все его потомки (дочерние элементы) должны быть повторно отрисованы (отрендерены) для обновления UI приложения. Повторный рендеринг — очень медленный процесс. Таким образом, чем больше у нас компонентов UI, тем более дорогими с точки зрения производительности являются обновления DOM.
Манипуляции с DOM являются сердцем современного интерактивного веба. К сожалению, они намного медленнее большинства JavaScript-операций. Ситуация усугубляется тем, что многие JavaScript-фреймворки обновляют DOM чаще, чем необходимо.
Допустим, у нас имеется список из 10 элементов. Мы изменяем первый элемент. Большинство фреймворков перестроят весь список. Это в 10 раз больше работы, чем требуется! Только 1 элемент изменился, остальные 9 остались прежними.
Перестроение списка — это легкая задача для браузера, но современные веб-сайты могут осуществлять огромное количество манипуляций с DOM. Поэтому неэффективное обновление часто становится серьезной проблемой. Для решения данной проблемы команда React популяризовала нечто под названием виртуальный (virtual) DOM (VDOM).
Виртуальный DOM
В React для каждого объекта настоящего DOM (далее — RDOM) существует соответствующий объект VDOM. VDOM — это объектное представление RDOM, его легковесная копия. VDOM содержит те же свойства, что и RDOM, но не может напрямую влиять на то, что отображается на экране.
Виртуальный DOM (VDOM) — это концепция программирования, где идеальное или «виртуальное» представление UI хранится в памяти и синхронизируется с «реальным» DOM, используемая такими библиотеками, как ReactDOM. Данный процесс называется согласованием (reconcilation).
Манипуляции с RDOM являются медленными. Манипуляции с VDOM намного быстрее, поскольку они не отображаются (отрисовываются) на экране. Манипуляции с VDOM похожи на работу с проектом (или планом) здания перед началом его возведения.
Почему VDOM является более быстрым?
Когда в UI добавляются новые элементы, создается VDOM в виде дерева. Каждый элемент является узлом этого дерева. При изменении состояния любого элемента, создается новое дерево. Затем это новое дерево сравнивается (diffed) со старым.
После этого вычисляется наиболее эффективный метод внесения изменений в RDOM. Цель данных вычислений состоит в минимизации количества операций, совершаемых с RDOM. Тем самым, уменьшаются накладные расходы, связанные с обновлением RDOM.
На изображениях ниже представлено виртуальное DOM-дерево и процесс согласования.
Красным цветом обозначены узлы, которые были обновлены. Эти узлы представляют элементы UI, состояние которых изменилось. После этого вычисляется разница между предыдущей и текущей версиями виртуального DOM-дерева. Затем все родительское поддерево подвергается повторному рендерингу для представления обновленного UI. Наконец, это обновленное дерево используется для обновления RDOM.
Как React использует VDOM?
После того, как мы рассмотрели, что такое VDOM, настало время поговорить о том, как он используется в React.
1. React использует паттерн проектирования «Наблюдатель» (observer) и реагирует на изменения состояния
В React каждая часть UI является компонентом и почти каждый компонент имеет состояние (state). При изменении состояния компонента, React обновляет VDOM. После обновления VDOM, React сравнивает его текущую версию с предыдущей. Этот процесс называется «поиском различий» (diffing).
После обнаружения объектов, изменившихся в VDOM, React обновляет соответствующие объекты в RDOM. Это существенно повышает производительность по сравнению с прямыми манипуляциями DOM. Именно это делает React высокопроизводительной библиотекой JavaScript.
2. React использует механизм пакетного (batch) обновления RDOM
Это также положительно влияет на производительность. Названный механизм предполагает отправку обновлений в виде пакетов (набора, серии) вместо отправки отдельного обновления при каждом изменении состояния.
Повторная отрисовка UI — самая затратная часть, React обеспечивает точечную и групповую перерисовку RDOM.
3. React использует эффективный алгоритм поиска различий
React использует эвристический O(n) (линейный) алгоритм, основываясь на двух предположениях:
- Два элемента разных типов приводят к построению разных деревьев
- Разработчик может обеспечить стабильность элементов между рендерингами посредством пропа key (ключ)
На практике эти предположения являются верными почти во всех случаях.
При сравнении двух деревьев, React начинает с корневых элементов. Дальнейшие операции зависят от типов этих элементов.
Элементы разных типов
- Если корневые элементы имеют разные типы, React уничтожает старое дерево и строит новое с нуля
- Вместе со старым деревом уничтожаются все старые узлы DOM. Экземпляры компонента получают componentWillUnmount() . При построении нового дерева, новые узлы DOM встраиваются в DOM. Экземпляры компонента получают сначала UNSAFE_componentWillMount() , затем componentDidMount() . Любое состояние, связанное со старым деревом, утрачивается
- Любые компоненты, являющиеся дочерними по отношению к корневому, размонтируются, их состояние уничтожается. Например, при сравнении:
Старый Counter будет уничтожен и создан заново.
Элементы одинакового типа
При сравнении двух элементов одинакового типа, React «смотрит» на атрибуты этих элементов. Узлы DOM сохраняются, изменяются только их атрибуты. Например:
После сравнения этих элементов будет обновлен только атрибут className .
После обработки узла DOM, React рекурсивно перебирает всех его потомков.
Рекурсивный перебор дочерних элементов
По умолчанию React перебирает два списка дочерних элементов DOM-узла и генерирует мутацию при обнаружении различий.
Например, при добавлении элемента в конец списка дочерних элементов, преобразование одного дерева в другое работает хорошо:
Обычно, вставка элемента в начало списка плохо влияет на производительность. Например, преобразование одного дерева в другое в данном случае будет работать плохо:
Использование ключей
Для решения данной проблемы React предоставляет атрибут (проп) key . Когда дочерние элементы имеют ключи, React использует их для сравнения потомков текущего и предыдущего узлов. Например, добавление ключей к элементам из последнего примера сделает преобразование деревьев намного более эффективным:
Теперь React знает, что элемент с ключом 0 является новым, а элементы с ключами 1 и 2 старыми.
На практике в качестве ключей, как правило, используются идентификаторы:
При отсутствии идентификаторов, их всегда можно добавить в модель данных или создать хэш на основе какой-либо части данных. Ключи должны быть уникальными среди соседних элементов, а не глобально.
В крайнем случае, в качестве ключей можно использовать индексы массива. Это работает хорошо только в том случае, если порядок элементов остается неизменным. Изменение порядка элементов будет медленным.
Изменение порядка элементов при использовании индексов в качестве ключей также может привести к проблемам с состоянием элементов. Экземпляры компонента обновляются и повторно используются на основе ключей. Если ключом является индекс, перемещение элемента приведет к изменению ключа. Как результат, состояние компонента для таких вещей, как неуправляемое поле для ввода данных, может смешаться и обновиться неожиданным образом.
Простыми словами: «Вы говорите React, в каком состоянии должен находиться UI, и он обеспечивает соответствие DOM этому состоянию. Преимущество такого подхода состоит в том, что вам, как разработчику, не нужно знать, как именно происходит изменение атрибутов, обработка событий и обновление DOM».
Все эти вещи абстрагируются React. Все, что вам нужно делать — это обновлять состояние компонента, об остальном позаботится React. Это обеспечивает очень хороший опыт разработки.
Поскольку «виртуальный DOM» — это в большей степени паттерн, нежели конкретная технология, данное понятие может означать разные вещи. В мире React «виртуальный DOM», обычно, ассоциируется с React-элементами, которые являются объектами, представляющими пользовательский интерфейс. Тем не менее, React также использует внутренние объекты, которые называются «волокнами» (fibers). В этих объектах хранится дополнительная информация о дереве компонентов. Fiber — это новый движок согласования, появившийся в React 16. Его основная цель заключается в обеспечении инкрементального рендеринга VDOM.
Как выглядит VDOM?
Название «виртуальный DOM» делает концепцию немного магической (мистической). На самом деле, VDOM — это обычный JavaScript-объект.
Представим, что у нас имеется такое DOM-дерево:
Это дерево может быть представлено в виде такого объекта:
const vdom = < tagName: 'html', children: [ < tagName: 'head' >, < tagName: 'body', children: [ < tagName: 'ul', attributes: < class: 'list' >, children: [ < tagName: 'li', attributes: < class: 'list_item' >, textContent: 'Элемент списка', >, // конец li ], >, // конец ul ], >, // конец body ], > // конец html
Это наш VDOM. Как и RDOM, он является объектным представлением HTML-документа (разметки). Однако, поскольку он представляет собой всего лишь объект, мы можем свободно и часто им манипулировать, не прикасаясь к RDOM без крайней необходимости.
Вместо использования одного объекта для всего документа, удобнее разделить его на небольшие секции. Например, мы можем выделить из нашего объекта компонент list , соответствующий неупорядоченному списку:
const list = < tagName: 'ul', attributes: < class: 'list' >, children: [ < tagName: 'li', attributes: < class: 'list_item' >, textContent: 'Элемент списка', >, ], >
VDOM под капотом
Теперь давайте поговорим о том, как VDOM решает проблему производительности и повторного использования.
Как мы выяснили ранее, VDOM может использоваться для обнаружения конкретных изменений, которые необходимо произвести в DOM. Вернемся к примеру с неупорядоченным списком и внесем в него те же изменения, которые мы делали с помощью DOM API.
Первым делом, нам нужна копия VDOM с изменениями, которые мы хотим осуществить. Поскольку нам не нужно использовать DOM API, мы можем просто создать новый объект.
const copy = < tagName: 'ul', attributes: < class: 'list' >, children: [ < tagName: 'li', attributes: < class: 'list_item' >, textContent: 'Первый элемент списка', >, < tagName: 'li', attributes: < class: 'list_item' >, textContent: 'Второй элемент списка', >, ], >
Данная копия используется для создания «различия» (diff) между оригинальным VDOM ( list ) и его обновленной версией. Diff может выглядеть следующим образом:
const diffs = [ < newNode: < /* новая версия первого элемента списка */ >, oldNode: < /* оригинальная версия первого элемента списка */ >, index: < /* индекс элемента в родительском списке */ >, >, < newNode: < /* второй элемент списка */ >, index: < /* . */ >, >, ]
Данный diff содержит инструкции по обновлению RDOM. После определения всех различий мы можем отправить их в DOM для выполнения необходимых обновлений.
Например, мы можем перебрать все различия и либо добавить нового потомка, либо обновить существующего в зависимости от различия:
const domElement = document.quesrySelector('list') diffs.forEach((diff) => < const newElement = document.createElement(diff.newNode.tagName) /* Добавляем атрибуты . */ if (diff.oldNode) < // Если имеется старая версия, заменяем ее новой domElement.replaceChild(diff.newNode, diff.oldNode) >else < // Если старая версия отсутствует, создаем новый узел domElement.append(diff.newNode) >>)
Обратите внимание, что это очень упрощенная версия того, как может работать VDOM.
VDOM и фреймворки
Обычно, мы имеем дело с VDOM при использовании фреймворков.
Копцепция VDOM используется такими фреймворками, как React и Vue для повышения производительности обновления DOM. Например, с помощью React наш компонент list может быть реализован следующим образом:
Для обновления списка достаточно создать новый шаблон и снова передать его ReactDOM.render() :
const newList = React.createElement( 'ul', < className: 'list' >, React.createElement( 'li', < className: 'list_item' >, 'Первый элемент списка' ), React.createElement('li', < className: 'list_item' >, 'Второй элемент списка') ) const timerId = setTimeout(() => < ReactDOM.render(newList, document.body) clearTimeout(timerId) >, 5000)
Поскольку React использует VDOM, даже несмотря на то, что мы повторно рендерим весь список, обновляются только фактически изменившиеся части.
Заключение
VDOM, определенно, заслуживает нашего внимания. Он предоставляет отличный способ отделения логики приложения от DOM-элементов, уменьшая вероятность непреднамеренного создания узких мест, связанных с манипуляцией DOM. Другие библиотеки так или иначе используют такой же подход, мы наблюдаем становление данной концепции в качестве предпочтительной стратегии разработки веб-приложений.
Подход, используемый Angular , который является фреймворком, благодаря которому одностраничные приложения (single page applications, SPA) обрели столь широкую известность, называется Dirty Model Checking (грязной проверкой моделей). Следует отметить, что DMC и VDOM не исключают друг друга. MVC-фреймворк вполне может использовать оба подхода. В случае с React это не имеет особого смысла, поскольку React — это, в конце концов, всего лишь библиотека для слоя представления (view).
Облачные VDS от Маклауд быстрые и безопасные.
Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!