Reconciliation (сверка)

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

Motivation (мотивация)

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

Существуют некоторые общие решения этой алгоритмической задачи создания минимального числа операций для преобразования одного дерева в другое. Однако современные алгоритмы имеют сложность в порядке O (n 3 ), где n - количество элементов в дереве.

Если бы мы использовали это в React, для отображения 1000 элементов потребуется порядка одного миллиарда сравнений. Это слишком дорого. Вместо этого React реализует эвристический алгоритм O (n), основанный на двух предположениях:

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

Дифференциальный алгоритм

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

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

Всякий раз, когда корневые элементы имеют разные типы, React будет разрушать старое дерево и строить новое дерево с нуля. Переход от <a> к <img>, или <Article> к <Comment> или от <Button> к <div>- любой из них приведет к полному восстановлению.

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

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

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

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

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

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

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

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

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

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

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

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

После обработки узла DOM, React затем рекурсирует на дочерних элементах.

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

Когда компонент обновляется, экземпляр остается неизменным, так что состояние поддерживается через визуализацию. React обновляет реквизиты экземпляра базового компонента, чтобы он соответствовал новому элементу, а также вызовы componentWillReceiveProps() и componentWillUpdate() базовый экземпляр.

Затем render() вызывается метод и алгоритм дифферирования рекурсирует по предыдущему результату и новому результату.

Рекурсия о потомках

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

Например, при добавлении элемента в конце детей преобразование между этими двумя деревьями работает хорошо:

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

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

Реакция будет соответствовать двум <li>first</li> деревьям, соответствовать двум <li>second</li> деревьям, а затем вставить <li>third</li> дерево. Если вы реализуете его наивно, вставка элемента в начале имеет худшую производительность. Например, преобразование между этими двумя деревьями работает плохо:

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

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

Реакция будет мутировать каждый ребенок вместо того, чтобы реализовать его, чтобы сохранить <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 к своей модели или хешу некоторых частей содержимого для генерации ключа. Ключ должен быть уникальным среди своих братьев и сестер, но не уникален во всем мире.

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

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

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

Tradeoffs (Компромиссы)

Важно помнить, что алгоритм согласования является деталью реализации. Реагент может повторить все приложение для каждого действия; конечный результат будет таким же. Чтобы быть ясным, в этом контексте повторный вызов означает призыв renderко всем компонентам, это не означает, что React размонтирует и перемонтирует их. Это применит только к различиям, вытекающим из правил, изложенных в предыдущих разделах.

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

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

  1. Алгоритм не будет пытаться сопоставить поддеревья разных типов компонентов. Если вы видите себя чередующимся между двумя типами компонентов с очень похожим выходом, вы можете сделать его одним и тем же. На практике мы не обнаружили, что это проблема.
  2. Ключи должны быть стабильными, предсказуемыми и уникальными. Нестабильные ключи (например, созданные Math.random()) заставят много экземпляров компонентов и узлов DOM быть излишне воссозданы, что может привести к ухудшению производительности и потерям в дочерних компонентах.