Прокачайся в код-ревью: для первых 50 участников — курс бесплатный

время чтения: 6 мин

React JS: что такое DOM и как устроен процесс согласования

Что такое Document Object Model и в чем разница между реальным и виртуальным DOM? В статье рассматриваются эти два понятия и то, как устроен процесс согласования.

Автор статьи — EPAM Senior Software Engineer Анжани Тарайил.

Другие статьи автора:

Что такое DOM?

DOM — это объектная модель документа (Document Object Model). Это своего рода упорядоченная версия HTML на веб-странице или в приложении. Она отображает весь пользовательский интерфейс (UI) веб-приложения в виде древовидной структуры:

Отображение HTML и DOM

Ниже приведена упрощенная схема работы рендеринга в браузере:

Как работает рендеринг

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

Как работает рендеринг, как происходят изменения в пользовательском интерфейсе

Сложности рендеринга

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

AngularJS — популярный JavaScript-фреймворк, использующий реальный DOM. Одним из его недостатков является медленная работа при рендеринге сложных страниц, поскольку при каждом изменении приходится обновлять все DOM-дерево.

Виртуальный DOM

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

React — фреймворк, который широко применяется и использует виртуальный DOM.

При добавлении новых элементов в UI создается виртуальный DOM в виде дерева. Каждый элемент является узлом этого дерева. При изменении состояния любого из элементов создается новое дерево виртуального DOM. Затем это дерево сравнивают, или «дифференцируют» (алгоритм дифференцирования) с предыдущим деревом виртуального DOM.

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

Алгоритм дифференцирования и эффективное сравнение деревьев

Алгоритм дифференцирования упрощает процесс обнаружения изменений в Document Object Model после манипуляций. Этот алгоритм играет важную роль в оптимизации сравнения обновленных виртуальных DOM с их снимками до модификации. Понимая, как происходит сравнение деревьев, мы видим, насколько эффективно работают обновления и рендеринг в таких фреймворках, как React.

  • Сравнение корневых элементов

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

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

Если корневые элементы различаются по типу, React уничтожает старое дерево и строит новое с нуля. Узлы старого DOM уничтожаются, что приводит к запуску команды componentWillUnmount() в составных экземплярах. Новое дерево включает узлы нового DOM, что приводит к выполнению команды UNSAFE_componentWillMount(), а затем componentDidMount(). Важно отметить, что при этом теряется вся информация, связанная со старым деревом.

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

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

<div className="before_changes" title="Changes" />

<div className="after_changes" title="Changes" />

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

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

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

React понимает, что изменяться должен только цветовой стиль, поэтому он не осуществляет изменение fontWeight.

  • Унифицированные компоненты

Когда компоненты обновляются, их экземпляры остаются неизменными, что обеспечивает непрерывность состояния всех рендеров. Для этого React обновляет пропсы базового экземпляра компонента, чтобы они отражали новый элемент. Затем на существующем экземпляре вызываются команды UNSAFE_componentWillReceiveProps(), UNSAFE_componentWillUpdate() и componentDidUpdate(). Вызывается команда render(), инициирующая рекурсивное применение алгоритма дифференцирования к предыдущему и текущему результатам.

  • Рекурсивная оценка дочерних узлов

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

До:

<ul>

  <li>apple</li>

  <li>orange</li>

</ul>

После:

<ul>

  <li>apple</li>

  <li>orange</li>

  <li>banana</li>

</ul>

В этом случае React согласовывает деревья <li>apple</li> и <li>orange</li>, а затем интегрирует дерево <li>banana</li>.

Однако вставка элемента в начало может привести к снижению эффективности при ненадлежащей реализации:

До:

<ul>

  <li>Tata</li>

  <li>Toyota</li>

</ul>

После:

<ul>

  <li>Mahindra</li>

  <li>Tata</li>

  <li>Toyota</li>

</ul>

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

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

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

До:

<ul>

  <li key="2911">Tata</li>

  <li key="2912">Toyota</li>

</ul>

После:

<ul>

  <li key="2910">Mahindra</li>

  <li key="2911">Tata</li>

  <li key="2912">Toyota</li>

</ul>

React определяет, что элемент с ключом '2910' новый, а элементы с ключами '2911' и '2912' были перемещены.

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

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

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

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

Согласователь Stack

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

Согласователь Stack состоит из двух основных фаз: монтирования и обновления. Во время монтирования он создает новый элемент DOM для каждого компонента React и затем присоединяет его к DOM-дереву. В процессе обновления согласователь Stack сравнивает старую и новую версии дерева компонентов и затем вносит необходимые изменения в DOM, отражая новое дерево.

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

Согласователь Stack — это простой, но эффективный способ обновления DOM при изменении компонентов React. Он используется в React 15 и более ранних версиях. В React 16 согласователь Stack был заменен на виртуальный согласователь DOM, который является более эффективным способом обновления DOM.

Некоторые из ключевых понятий согласователя Stack включают:

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

Согласователь Fiber

Согласователь Fiber — это новый алгоритм согласования, представленный в React 16. Он предназначен для повышения эффективности и масштабируемости приложений React за счет реализации асинхронного рендеринга и нового алгоритма планирования.

Основное различие между согласователями Stack и Fiber заключается в том, что Fiber разбивает процесс согласования на более мелкие фрагменты, так называемые волокна.

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

Некоторые ключевые особенности согласователя Fiber:

  • Волокна — это небольшие блоки работы, которые представляют компонент в дереве React. Каждое волокно имеет ссылку на родительское волокно, дочерние волокна и связанный с ними узел DOM.
  • Ворклеты представляют собой асинхронные функции, которые могут быть запланированы для выполнения во время согласования. Это позволяет React асинхронно выполнять ресурсоемкие операции, такие как анимация и декодирование изображений, без блокирования основного потока.
  • Fiber использует новый алгоритм планирования для определения приоритетов обновлений в зависимости от их важности. Таким образом, сначала выполняется рендеринг критически важных обновлений, а менее важные могут быть отложены на более поздний срок.
  • Согласователь Fiber — более сложный алгоритм, чем согласователь Stack, но он значительно повышает эффективность и масштабируемость. Он по умолчанию используется как алгоритмом согласования в React 16 и более поздних версиях.

Коротко об основных различиях между согласователями Fiber и Stack:

Различия между согласователями Fiber и Stack

Заключение

Реальный DOM представляет собой структуру веб-страницы, а виртуальный DOM — это легковесная абстракция, которую React использует для эффективного обновления. Согласование в React — это метод, с помощью которого пользовательский интерфейс обновляется в соответствии с изменениями в состоянии компонента. Алгоритм согласования включает в себя рекомендации, которые React использует для определения наиболее ресурсоэффективного подхода к обновлению UI. Для управления обновлениями UI React использует виртуальный DOM. Все это делает React высокоэффективной библиотекой для построения пользовательских интерфейсов.

React-разработчик: источники изучения и перспективы в будущем