Как стать автором
Обновить
АО «ГНИВЦ»
Драйвер цифровой трансформации

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

Уровень сложностиСредний
Время на прочтение7 мин
Количество просмотров1.9K

Привет, меня зовут Дмитрий, я React-разработчик и в статье хочу снова рассмотреть тему, которая у всех на слуху, однако «подкапотной» информации по ней не так много. Всем известно, что React обновляет компоненты, когда это необходимо, но как это происходит на самом деле, «под капотом»? Постараюсь описать суть простыми словами. Давайте разберемся вместе — что мне удалось узнать.

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

Как работает reconciliation в общих чертах

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

Дальше происходит процесс сравнения. React сравнивает новый виртуальный DOM с предыдущим. Этот процесс называется diffing. Он позволяет определить, какие элементы были добавлены, удалены или изменены.

После процесса diffing-а React создаёт список изменений (effect list), который применяется на Commit Phase.

А как под капотом работает Reconciliation?

Под капотом Reconciliation в React основан на алгоритме Fiber, который обеспечивает эффективное обновление интерфейса, разбивая обновления на небольшие части и выполняя их итеративно.

Фазы Reconciliation

Reconciliation в React делится на две основные фазы:

1. Фаза Render Phase (Diffing) —  вычисление изменений

Здесь создается новое Work-In-Progress (WIP) Fiber-дерево, которое используется для вычисления изменений. Оговорюсь, что Fiber-дерево существует всегда, но React создает его новую версию во время рендера. React использует алгоритм diffing, чтобы сравнить новое WIP-дерево с текущим. Вычисляется, какие изменения нужно применить к реальному DOM. Эта фаза может быть прервана при использовании Concurrent Mode, если есть более приоритетные задачи (например, пользовательский ввод). В обычном синхронном рендеринге фаза рендера не прерывается.

2. Фаза Commit Phase (Patching) - обновление DOM

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

Что за процесс Diffing такой?

Diffing делает следующие последовательные шаги:

1. Сравнение типов элементов

На первом шаге React сначала проверяет: изменился ли тип корневого элемента.

<div>Hello</div> → <span>Hello</span>

Так как, например, если по каким-то причинам тег <div> заменился на <span>, то React удалит весь узел <div> и создаст новый <span>, даже если текст внутри остался тем же.

Дальше, если тип элемента не изменился, проверяются его атрибуты:

<div className="red">Hello</div> → <div className="blue">Hello</div>

Алгоритм просто изменит className, не затрагивая текстовый контент.

2. Рекурсивное сравнение дочерних элементов

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

<div><h1>Привет</h1></div> → <div><h1>Пока</h1></div>

Например, в данном случае изменится только текст в теге<h1>, не затрагивая родительский <div>.

3. Оптимизация списков с помощью ключей (keys)

Если, например, список рендерится через .map(), то все мы знаем, что нужно юзать уникальные ключи, которые React использует для отслеживания элементов.

Код без использования ключей:

<ul>
  {items.map((item) => (
    <li>{item.text}</li>
  ))}
</ul>

Если забыть прописать ключи React ругнется, а при изменении массива пересоздадутся все теги <li>, даже если часть элементов осталась неизменной. Если не использовать key, React будет сравнивать элементы по позиции, что приводит к неправильным обновлениям. Собственно, отсюда и правило: нельзя использовать index в качестве ключа. Ключи должны быть уникальными.

Правильный код с использованием ключей:

<ul>
  {items.map((item) => (
    <li key={item.id}>{item.text}</li>
  ))}
</ul>

Теперь React будет точно знать, что изменять нужно только те <li>, у которых key изменился, а остальные оставить без изменений.

Подробнее про React Fiber

React Fiber — это переписанный алгоритм согласования, представленный в React 16, который позволяет управлять рендерингом эффективней старого стекового алгоритма.

До React 16 использовался стековый рекурсивный алгоритм. Проблемы заключалась в том, что, если один компонент рендерился долго, UI мог «зависнуть». Отсутствовали приоритеты рендеринга, а также не было возможности прервать рендеринг, например, если пользователь вводил текст, то React мог продолжать рендеринг и не реагировать на ввод.

Fiber решил все эти проблемы и теперь у нас есть:

  • Приоритизация задач. Сначала перерендериваем более важные части, потом менее.

  • Разбиение рендеринга на чанки (React может рендерить части дерева отдельно).

  • Асинхронный рендеринг (React может прерывать выполнение и продолжать позже).

Концепции Fiber

Каждый узел VDOM — это Fiber Node
В отличие от старого алгоритма, React теперь хранит в памяти две версии дерева. Первая — это текущее (current) дерево — то, что сейчас в DOM, вторая — рабочее (work‑in‑progress) дерево — новая версия, которую React готовит.

Рендеринг разбивается на этапы
Рендеринг разбивается на этапы с использованием «chunk‑based rendering». React может приостанавливать рендеринг для выполнения более важных задач, таких как пользовательский ввод, и продолжить его позже.

Фазы работы Fiber

Render Phase (Diffing, вычисление изменений)

  • Проходит асинхронно и может быть прервано.

  • Создаётся новое Work-In-Progress (WIP) Fiber-дерево.

Commit Phase (Обновление DOM)

  • Выполняется синхронно и не может быть прервано.

  • Применяются изменения к DOM и вызываются componentDidMount, componentDidUpdate, useEffect.

Структура Fiber Node

Структура Fiber Node в React представляет собой описание каждого узла виртуального DOM (VDOM) и содержит информацию, необходимую для эффективного обновления реального DOM. Ноды хранятся в памяти и служат промежуточным представлением элемента, которое позволяет React управлять рендерингом и обновлением интерфейса. Вот так выглядит структура:

{
  type: 'div',
  stateNode: DOMNode,
  child: FiberNode,
  sibling: FiberNode,
  return: FiberNode,
  effectTag: 'update',
  pendingProps: {...},
  memoizedProps: {...},
  alternate: FiberNode,
  updateQueue: null,
  hooks: null,
  index: 0,
  nextEffect: FiberNode,
  firstEffect: FiberNode,
  lastEffect: FiberNode,
  tag: 'FunctionComponent',
  mode: 0
}
  • type: Тип узла, который определяет, какой элемент должен быть отрендерен.

  • stateNode: Ссылка на реальный DOM-узел, с которым этот Fiber связан.

  • child: Ссылка на первый дочерний узел, если он есть.

  • sibling: Ссылка на следующий узел этого же уровня.

  • return: Ссылка на родительский узел. Указывает на Fiber-узел родителя этого компонента, строя дерево.

  • effectTag: Тип обновления.

  • pendingProps: Пропсы, которые должны быть применены к этому узлу в процессе рендера.

  • memoizedProps: Пропсы, которые использовались для рендеринга этого компонента в прошлый раз.

  • alternate: Ссылка на Fiber-узел, который хранит старую версию Fiber-узла для вычисления изменений.

  • updateQueue: Очередь обновлений для компонента.

  • hooks: Информация о хуках (если компонент является функциональным).

  • index: Индекс элемента в родительском списке. Используется для оптимизации рендеринга списков и определения порядка обновлений.

  • nextEffect: Ссылка на следующий эффект в списке эффектов.

  • firstEffect: Ссылка на первый эффект в списке.

  • lastEffect: Ссылка на последний эффект в списке.

  • tag: Тип компонента.

  • mode: Режим рендеринга, который описывает, как компонент будет обновляться (например, в Concurrent Mode или Strict Mode).

Как Fiber приоритизирует рендеринг?

Fiber использовал систему приоритетов expirationTime (в последних версиях используется Scheduler с приоритетами, такими как Immediate, User-blocking, Normal, Low и Idle). Согласно ей, он отправляет на перерендер сначала важные изменения, потом менее важные.

Важными событиями считаются пользовательские события onChange, onClick и т.п.

События сосредней важностью — это анимации, переходы. Анимации и переходы работают через Concurrent Mode и могут быть прерваны.

Менее важными — побочные эффекты UseEffect. React может отложить обновления менее важного UI, если активно обрабатывает другие задачи.

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

Сначала React запускает рендеринг (Render Phase), далее создается новое Work-In-Progress (WIP) Fiber-дерево. Здесь React не вносит изменения в DOM, а только создает описание изменений.

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

И конечным действием React вносит изменения в реальный DOM, обновляя только те узлы, которые изменились. На этом этапе вызываются lifecycle-методы: componentDidMount, componentDidUpdate, а также хуки useEffect.

Пример:

function App() {
  const [count, setCount] = React.useState(0);
  
  return (
    <div>
      <h1>Счетчик: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Увеличить</button>
    </div>
  );
}

В примере при изменении count происходит следующее:

Сначала React создает Work-In-Progress дерево. Затем сравнивает новое дерево с предыдущим, где видит, что тег <h1> изменился и помечает его как «нужно обновить».

Далее React запускает Commit Phase. Обновляет innerText у тега <h1>.

Готово. React не будет трогать тег <button>, потому что он не изменился.

Производительность

Что касается производительности всего этого процесса, то для того, чтобы его ускорить и оптимизировать, нужно свести количество перерендеров к минимуму. В этом помогут:
React.memo, useMemo, useCallback и правильное использование ключей в списках.

Но об этом все уже знают, это тема отдельная, однако стоило упомянуть это в контексте оптимизации.

Итог

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

Теги:
Хабы:
+6
Комментарии0

Публикации

Информация

Сайт
www.gnivc.ru
Дата регистрации
Дата основания
1977
Численность
1 001–5 000 человек
Местоположение
Россия