Полное руководство по виртуальной DOM React

Полное руководство по виртуальной DOM React

От автора: борьба с бесполезными манипуляциями с DOM, согласование и алгоритм различий.

Реальная DOM

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

Что замедляет манипуляции с DOM?

Модель DOM представлена в виде древовидной структуры данных. Из-за этого изменения и обновления в DOM происходят быстро. Но после изменения обновленный элемент и его дочерние элементы должны быть повторно отрисованы, чтобы обновить пользовательский интерфейс приложения. Повторный рендеринг или перерисовка пользовательского интерфейса — вот что делает его медленным. Следовательно, чем больше у вас компонентов пользовательского интерфейса, тем дороже могут быть обновления DOM, поскольку их нужно будет повторно отображать при каждом обновлении DOM.

Манипуляции с DOM — это сердце современного интерактивного Интернета. К сожалению, это также намного медленнее, чем большинство операций JavaScript. Эта медлительность усугубляется тем фактом, что большинство фреймворков JavaScript обновляют DOM гораздо чаще, чем необходимо.

В качестве примера предположим, что у вас есть список из десяти элементов. Вы отмечаете первый пункт. Большинство фреймворков JavaScript перестраивают весь список. Это в десять раз больше работы, чем необходимо! Изменился только один элемент, а остальные девять будут восстановлены точно такими же, как и раньше.

Перестроение списка не представляет большого труда для веб-браузера, но современные веб-сайты могут использовать огромное количество манипуляций с DOM. Неэффективное обновление стало серьезной проблемой. Чтобы решить эту проблему, люди которые работают с React популяризировали нечто, называемое виртуальным DOM.

Виртуальная DOM

В React для каждого объекта DOM существует соответствующий «виртуальный объект DOM». Виртуальный объект DOM — это представление объекта DOM, подобно его облегченной копии. Виртуальный объект DOM имеет те же свойства, что и реальный объект DOM, но у него нет реальной возможности напрямую изменять то, что отображается на экране.

«Виртуальная модель DOM (VDOM) — это концепция программирования, в которой идеальное или «виртуальное» представление пользовательского интерфейса хранится в памяти и синхронизируется с« реальной » библиотекой DOM, такой как ReactDOM. Этот процесс называется согласованием».

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

Как виртуальный DOM работает быстрее?

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

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

На изображении ниже показано виртуальное дерево DOM и процесс сравнения.

Полное руководство по виртуальной DOM React

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

Полное руководство по виртуальной DOM React

Как React использует виртуальную модель DOM?

Теперь, когда у вас есть четкое представление о том, что такое виртуальная модель DOM и как она может повысить производительность вашего приложения, давайте посмотрим, как React использует виртуальную модель DOM.

1. React следует шаблону наблюдателю и отслеживает изменения состояния.

В React каждая часть пользовательского интерфейса является компонентом, и каждый компонент имеет состояние. Когда состояние компонента изменяется, React обновляет виртуальное дерево DOM. После обновления виртуальной DOM, React сравнивает текущую версию виртуальной DOM с предыдущей версией виртуальной DOM. Этот процесс называется «дифференцированием».

Как только React узнает, какие виртуальные объекты DOM были изменены, он обновляет только эти объекты в реальной DOM. Это значительно повышает производительность по сравнению с непосредственным манипулированием реальной DOM. Это выделяет React как высокопроизводительную библиотеку JavaScript.

2. React следует механизму пакетного обновления для обновления реальной DOM.

Следовательно — приводит к увеличению производительности. Это означает, что обновления реальной модели DOM отправляются пакетами вместо отправки обновлений для каждого отдельного изменения состояния.

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

3. React следует эффективному алгоритму различий.

React реализует эвристический алгоритм O (n), основанный на двух предположениях:

Два элемента разных типов будут давать разные деревья.

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

На практике эти предположения верны почти для всех случаев использования. При рассмотрении двух деревьев React сначала сравнивает два корневых элемента. Поведение различается в зависимости от типов корневых элементов.

Элементы разных типов

Всякий раз, когда корневые элементы имеют разные типы, React удаляет старое дерево и строит новое дерево с нуля.

При удалении дерева старые узлы DOM уничтожаются. Экземпляры компонентов получают состояние componentWillUnmount(). При построении нового дерева новые узлы DOM вставляются в DOM. Экземпляры компонентов получают состояние UNSAFE_componentWillMount() а затем componentDidMount(). Любое состояние, связанное со старым деревом, теряется.

Любые компоненты ниже корневого также будут размонтированы, и их состояние будет уничтожено. Пример, для сравнения:

<div> <Counter />
</div> <span> <Counter />
</span>

Это уничтожит старый Counter и перемонтирует новый.

Элементы одного типа

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

<div className="before" title="stuff" /><div className="after" title="stuff" />

Сравнивая эти два элемента, React знает, что нужно изменять только базовый узел className. При обновлении style, React также знает, что нужно обновлять только те свойства, которые изменились. Например:

<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />

При преобразовании между этими двумя элементами React знает, что нужно изменять только стиль color, а не fontWeight. После обработки узла DOM React рекурсивно обращается к дочерним элементам.

Рекурсия по дочерним элементам

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

<ul> <li>first</li> <li>second</li>
</ul>
<ul> <li>first</li> <li>second</li> <li>third</li>
</ul>

React сопоставит два дерева <li>first</li>, сопоставит два дерева <li>second</li>, а затем вставит дерево <li>third</li>.

Если вы реализуете это без Virtual DOM, вставка элемента в начале имеет худшую производительность. Например, преобразование между этими двумя деревьями работает плохо:

<ul> <li>Duke</li> <li>Villanova</li>
</ul>
<ul> <li>Connecticut</li> <li>Duke</li> <li>Villanova</li>
</ul>

React будет видоизменять каждый дочерний элемент вместо того, чтобы взять во внимание, что он может сохранить нетронутыми поддеревья <li>Duke</li> и <li>Villanova</li>. Эта неэффективность может стать проблемой.

Использование ключей

Чтобы решить эту проблему, React поддерживает атрибут key. Когда у потомков есть ключи, React использует ключ для сопоставления их в исходном дереве с потомками в последующем дереве. Например, добавление key к нашему неэффективному примеру выше может сделать преобразование дерева эффективным:

<ul> <li key="2015">Duke</li> <li key="2016">Villanova</li>
</ul>
<ul> <li key="2014">Connecticut</li> <li key="2015">Duke</li> <li key="2016">Villanova</li>
</ul>

Теперь React знает, что элемент с ключом ’2014′ является новым, а элементы с ключами ’2015′ и ’2016′ только переместились.

На практике найти ключ обычно не сложно. Элемент, который вы собираетесь отображать, может уже иметь уникальный идентификатор, поэтому ключ может быть просто получен из ваших данных:

<li key={item.id}>{item.name}</li>

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

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

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

Проще говоря: «Вы сообщаете React, в каком состоянии вы хотите, чтобы находился пользовательский интерфейс, и он гарантирует, что DOM соответствует этому состоянию. Большим преимуществом здесь является то, что вам как разработчику не нужно знать, как делать манипуляции с атрибутами, обработку событий или ручное обновление DOM за кулисами».

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

Поскольку «виртуальный DOM» — это скорее шаблон, чем конкретная технология, люди иногда говорят, что это означает разные вещи. В мире React термин «виртуальный DOM» обычно ассоциируется с элементами React, поскольку они являются объектами, представляющими пользовательский интерфейс. Однако React также использует внутренние объекты, называемые «волокнами», для хранения дополнительной информации о дереве компонентов. Их также можно рассматривать как часть реализации «виртуальной DOM» в React. Fiber — это новый механизм согласования в React 16. Его основная цель — включить инкрементный рендеринг виртуальной DOM.

Как выглядит виртуальная модель DOM?

Название «виртуальный DOM», как правило, добавляет загадочности тому, что это за концепция на самом деле. Фактически, виртуальная модель DOM — это просто обычный объект Javascript. Вернемся к дереву DOM, которое мы создали ранее:

Полное руководство по виртуальной DOM React

Это дерево также может быть представлено как объект Javascript.

const vdom = { tagName: "html", children: [ { tagName: "head" }, { tagName: "body", children: [ { tagName: "ul", attributes: { "class": "list" }, children: [ { tagName: "li", attributes: { "class": "list__item" }, textContent: "List item" } // end li ] } // end ul ] } // end body ]
} // end html

Мы можем думать об этом объекте как о нашем виртуальном DOM. Как и исходная модель DOM, это объектное представление нашего HTML-документа. Но поскольку это простой объект Javascript, мы можем свободно и часто манипулировать им, не касаясь фактического DOM до тех пор, пока нам это не понадобится.

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

const list = { tagName: "ul", attributes: { "class": "list" }, children: [ { tagName: "li", attributes: { "class": "list__item" }, textContent: "List item" } ]
};

Под капотом виртуальной DOM

Теперь, когда мы увидели, как выглядит виртуальная модель DOM, как она работает для решения проблем производительности и удобства использования модели DOM?

Как я уже упоминал, мы можем использовать виртуальную DOM, чтобы выделить конкретные изменения, которые необходимо внести в DOM, и сделать эти конкретные обновления самостоятельно. Вернемся к нашему примеру с неупорядоченным списком и внесем те же изменения, которые мы сделали с помощью DOM API.

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

const copy = { tagName: "ul", attributes: { "class": "list" }, children: [ { tagName: "li", attributes: { "class": "list__item" }, textContent: "List item one" }, { tagName: "li", attributes: { "class": "list__item" }, textContent: "List item two" } ]
};

Команда copy используется для создания так называемой «разницы» между исходной виртуальной DOM, в данном случае list, и обновленной. Дифференциал может выглядеть примерно так:

const diffs = [ { newNode: { /* new version of list item one */ }, oldNode: { /* original version of list item one */ }, index:/* index of element in parent's list of child nodes */ }, { newNode: { /* list item two */ }, index: { /* */ } }
]

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

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

const domElement = document.getElementsByClassName("list")[0];
diffs.forEach((diff) => { const newElement = document.createElement(diff.newNode.tagName); /* Add attributes ... */ if (diff.oldNode) { // If there is an old version, replace it with the new version domElement.replaceChild(diff.newNode, diff.index); } else { // If no old version exists, create a new node domElement.appendChild(diff.newNode); }
})

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

Виртуальный DOM и фреймворки

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

Такие фреймворки, как React и Vue, используют концепцию виртуальной DOM для более производительных обновлений DOM. Например, наш компонент list можно написать на React следующим образом.

import React from 'react';
import ReactDOM from 'react-dom';
const list = React.createElement("ul", { className: "list" }, React.createElement("li", { className: "list__item" }, "List item")
);
ReactDOM.render(list, document.body);

Если бы мы хотели обновить наш список, мы могли бы просто переписать весь шаблон списка и вызвать ReactDOM.render() снова, передав новый список.

const newList = React.createElement("ul", { className: "list" }, React.createElement("li", { className: "list__item" }, "List item one"), React.createElement("li", { className: "list__item" }, "List item two");
);
setTimeout(() => ReactDOM.render(newList, document.body), 5000);

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

Полное руководство по виртуальной DOM React

Заключение

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

Используемый подход Angular, который, возможно, является фреймворком, который популяризировал концепцию SPA (одностраничных приложений), называется Dirty Model Checking. Стоит отметить, что dirty model checking и virtual DOM это не взаимоисключающие вещи. Оба они являются решением одной и той же проблемы, но решают ее по-разному. Структура MVC вполне может реализовать оба метода. В случае с React это просто не имело особого смысла — React — это по большей части библиотека для отображения Представления.

Надеюсь, что благодаря этому вы почувствуете себя более комфортно с «Virtual DOM». Спасибо за чтение.

Автор: Ayush Verma

Источник: javascript.plainenglish.io

Редакция: Команда webformyself.

Читайте нас в Telegram, VK, Яндекс.Дзен