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

Привет, меня зовут Дмитрий, я 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 сначала проверяет: изменился ли тип корневого элемента.
Hello → HelloТак как, например, если по каким-то причинам тег
Дальше, если тип элемента не изменился, проверяются его атрибуты:
Hello → HelloАлгоритм просто изменит className, не затрагивая текстовый контент.
2. Рекурсивное сравнение дочерних элементов
На втором шаге рекурсивно сравниваются дочерние элементы. Если структура осталась прежней, он обновит только измененные узлы.
Привет
→ Пока
Например, в данном случае изменится только текст в теге Если, например, список рендерится через Код без использования ключей: Если забыть прописать ключи React ругнется, а при изменении массива пересоздадутся все теги Правильный код с использованием ключей: Теперь React будет точно знать, что изменять нужно только те React Fiber — это переписанный алгоритм согласования, представленный в React 16, который позволяет управлять рендерингом эффективней старого стекового алгоритма. До React 16 использовался стековый рекурсивный алгоритм. Проблемы заключалась в том, что, если один компонент рендерился долго, UI мог «зависнуть». Отсутствовали приоритеты рендеринга, а также не было возможности прервать рендеринг, например, если пользователь вводил текст, то React мог продолжать рендеринг и не реагировать на ввод. Fiber решил все эти проблемы и теперь у нас есть: Приоритизация задач. Сначала перерендериваем более важные части, потом менее. Разбиение рендеринга на чанки (React может рендерить части дерева отдельно). Асинхронный рендеринг (React может прерывать выполнение и продолжать позже). Приоритизация задач. Сначала перерендериваем более важные части, потом менее. Разбиение рендеринга на чанки (React может рендерить части дерева отдельно). Асинхронный рендеринг (React может прерывать выполнение и продолжать позже). Каждый узел VDOM — это Fiber Node Рендеринг разбивается на этапы Render Phase (Diffing, вычисление изменений) Проходит асинхронно и может быть прервано. Создаётся новое Work-In-Progress (WIP) Fiber-дерево. Commit Phase (Обновление DOM) Выполняется синхронно и не может быть прервано. Применяются изменения к DOM и вызываются componentDidMount, componentDidUpdate, useEffect. Структура Fiber Node в React представляет собой описание каждого узла виртуального DOM (VDOM) и содержит информацию, необходимую для эффективного обновления реального DOM. Нода хранятся в памяти и служат промежуточным представлением элемента, которое позволяет React управлять рендерингом и обновлением интерфейса. Вот так выглядит структура: 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). 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 использовал систему приоритетов expirationTime (в последних версиях используется Scheduler с приоритетами, такими как Immediate, User-blocking, Normal, Low и Idle). Согласно ей, он отправляет на перерендер сначала важные изменения, потом менее важные. Важными событиями считаются пользовательские события onChange, onClick и т.п. События со средней важностью — это анимации, переходы. Анимации и переходы работают через Concurrent Mode и могут быть прерваны. Менее важными — побочные эффекты UseEffect. React может отложить обновления менее важного UI, если активно обрабатывает другие задачи. Сначала React запускает рендеринг (Render Phase), далее создается новое Work-In-Progress (WIP) Fiber-дерево. Здесь React не вносит изменения в DOM, а только создает описание изменений. Следующим действием идет сравнение нового WIP-дерева со старым деревом и отмечаются изменения, которые нужно внести. И конечным действием React вносит изменения в реальный DOM, обновляя только те узлы, которые изменились. На этом этапе вызываются lifecycle-методы: componentDidMount, componentDidUpdate, а также хуки useEffect. Пример: В примере при изменении count происходит следующее: Сначала React создает Work-In-Progress дерево. Затем сравнивает новое дерево с предыдущим, где видит, что тег Далее React запускает Commit Phase. Обновляет innerText у тега Готово. React не будет трогать тег Что касается производительности всего этого процесса, то для того, чтобы его ускорить и оптимизировать, нужно свести количество перерендеров к минимуму. В этом помогут: Но об этом все уже знают, это тема отдельная, однако стоило упомянуть это в контексте оптимизации. В этой статье я попытался собрать информацию по процессу Reconciliation в React, разобрав его основные аспекты и детали работы под капотом. Надеюсь, что мой материал поможет вам лучше понять, как React обновляет интерфейс и какие технологии стоят за этим процессом. Если у вас есть замечания, вопросы или дополнения, буду рад услышать их в комментариях! , не затрагивая родительский 3. Оптимизация списков с помощью ключей (keys)
.map(), то все мы знаем, что нужно юзать уникальные ключи, которые React использует для отслеживания элементов.
{items.map((item) => (
, даже если часть элементов осталась неизменной. Если не использовать key, React будет сравнивать элементы по позиции, что приводит к неправильным обновлениям. Собственно, отсюда и правило: нельзя использовать index в качестве ключа. Ключи должны быть уникальными.
{items.map((item) => (
, у которых key изменился, а остальные оставить без изменений.Подробнее про React Fiber
Концепции Fiber
В отличие от старого алгоритма, React теперь хранит в памяти две версии дерева. Первая — это текущее (current) дерево — то, что сейчас в DOM, вторая — рабочее (work-in-progress) дерево — новая версия, которую React готовит.
Рендеринг разбивается на этапы с использованием «chunk-based rendering». React может приостанавливать рендеринг для выполнения более важных задач, таких как пользовательский ввод, и продолжить его позже.Фазы работы Fiber
Структура Fiber Node
{
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
}Как Fiber приоритизирует рендеринг?
Как работает рендеринг в Fiber?
function App() {
const [count, setCount] = React.useState(0);
return (
Счетчик: {count}
изменился и помечает его как «нужно обновить».., потому что он не изменился.Производительность
React.memo, useMemo, useCallback и правильное использование ключей в списках.Итог
Habrahabr.ru прочитано 10653 раза
