Примирение
React предоставляет декларативный API, чтобы вы не беспокоились о том, какие именно изменения происходят при каждом обновлении. Это значительно упрощает написание приложений, но не всегда очевидно, как это реализовано в React. Эта статья объясняет принятые в React решения в алгоритме «примирения» (diffing), чтобы обновления компонентов были предсказуемыми и достаточно быстрыми для приложений с высокой производительностью.
Мотивация
Когда вы используете React, в определенный момент времени можно представить себе функцию render() как создающую дерево React-элементов. При следующем обновлении состояния или свойств (props) эта функция render() вернёт другое дерево React-элементов. React затем должен определить, как эффективно обновить пользовательский интерфейс, чтобы он соответствовал последнему дереву.
Существуют общие решения для этой алгоритмической задачи создания минимального количества операций для преобразования одного дерева в другое. Однако, алгоритмы «лучшего состояния» имеют сложность порядка O(n3), где n — количество элементов в дереве.
Если бы мы использовали этот алгоритм в React, для отображения 1000 элементов потребовалось бы порядка миллиарда сравнений. Это слишком дорого. Вместо этого React использует эвристический алгоритм O(n) на основе двух предположений:
- Два элемента разных типов приведут к разным деревьям.
- Разработчик может указать, какие дочерние элементы могут быть стабильными при разных рендерах с помощью свойства
key.
На практике эти предположения справедливы практически для всех практических случаев.
Алгоритм примирения
При примирении двух деревьев React сначала сравнивает два корневых элемента. Поведение различается в зависимости от типов корневых элементов.
Элементы разных типов
Всякий раз, когда корневые элементы имеют разные типы, React разрушит старое дерево и построит новое дерево с нуля. Переход от <a> к <img>, или от <Article> к <Comment>, или от <Button> к <div> — всё это приведёт к полной перестройке.
При разрушении дерева старые узлы DOM уничтожаются. Экземпляры компонентов получают componentWillUnmount(). При построении нового дерева новые узлы DOM вставляются в DOM. Экземпляры компонентов получают UNSAFE_componentWillMount() и затем componentDidMount(). Любое состояние, связанное со старым деревом, теряется.
Все компоненты ниже корня также будут размонтированы, и их состояние будет уничтожено. Например, при примирении:
<div> <Counter /> </div> <span> <Counter /> </span>
Это приведёт к уничтожению старого Counter и повторному монтированию нового.
Примечание:
Эти методы считаются устаревшими, и в новом коде следует избегать их:
UNSAFE_componentWillMount()
Элементы DOM одного типа
При сравнении двух элементов React DOM одного типа React проверяет атрибуты обоих, сохраняет тот же базовый узел DOM и обновляет только изменённые атрибуты. Например:
<div className="before" title="stuff" /> <div className="after" title="stuff" />
Сравнивая эти два элемента, React знает, что нужно изменить только className в базовом узле DOM.
При обновлении style, React также знает, что нужно обновить только те свойства, которые изменились. Например:
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} /> При преобразовании между этими двумя элементами React знает, что нужно изменить только стиль color, а не fontWeight.
После обработки узла DOM React рекурсивно обрабатывает потомков.
Элементы компонентов одного типа
Когда компонент обновляется, экземпляр остаётся тем же, так что состояние сохраняется при каждом рендере. React обновляет свойства (props) экземпляра базового компонента, чтобы соответствовать новому элементу, и вызывает UNSAFE_componentWillReceiveProps(), UNSAFE_componentWillUpdate() и componentDidUpdate() в базовом экземпляре.
Затем вызывается метод render(), и алгоритм примирения рекурсивно обрабатывает предыдущий результат и новый результат.
Примечание:
Эти методы считаются устаревшими, и в новом коде следует избегать их:
UNSAFE_componentWillUpdate()UNSAFE_componentWillReceiveProps()
Рекурсивное обращение с потомками
По умолчанию, при рекурсивном обращении с потомками узла 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> дерево.
Если реализовать это наивно, вставка элемента в начало имеет худшую производительность. Например, преобразование между этими двумя деревьями работает плохо:
<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 в свою модель или хешировать части контента, чтобы сгенерировать ключ. Ключ должен быть уникальным только среди своих братьев и сестёр, а не глобально уникальным.
В крайнем случае, вы можете передать индекс элемента в массиве в качестве ключа. Это может хорошо работать, если элементы никогда не переупорядочиваются, но переупорядочения будут медленными.
Переупорядочения также могут вызвать проблемы со состоянием компонента, когда индексы используются в качестве ключей. Экземпляры компонентов обновляются и повторно используются на основе их ключа. Если ключ — это индекс, перемещение элемента его меняет. В результате состояние компонента для элементов, таких как неуправляемые входные данные, может быть перемешано и обновлено неожиданными способами.
Вот пример проблем, которые могут быть вызваны использованием индексов в качестве ключей на CodePen, и вот обновлённая версия того же примера, показывающая, как отказ от использования индексов в качестве ключей решит эти проблемы с переупорядочиванием, сортировкой и вставкой элементов в начало.
Компромиссы
Важно помнить, что алгоритм примирения — это деталь реализации. React мог бы перерисовывать всё приложение при каждом действии; конечный результат был бы тем же самым. Для ясности, перерисовка в данном контексте означает вызов render для всех компонентов, а не означает, что React будет размонтировать и заново монтировать их. Он будет применять только различия, следуя правилам, указанным в предыдущих разделах.
Мы регулярно усовершенствуем эвристики, чтобы ускорить общие случаи использования. В текущей реализации вы можете указать, что поддерево было перемещено среди своих братьев и сестёр, но не можете указать, что оно переместилось куда-то ещё. Алгоритм перерисует это полное поддерево.
Поскольку React полагается на эвристики, если предположения, лежащие в их основе, не выполняются, производительность пострадает.
- Алгоритм не будет пытаться сопоставить поддеревья различных типов компонентов. Если вы обнаружите, что чередуете два компонента с очень похожим выводом, вы можете сделать их одного типа. На практике мы не сталкивались с этой проблемой.
- Ключи должны быть стабильными, предсказуемыми и уникальными. Нестабильные ключи (такие как те, которые генерируются
Math.random()) приведут к ненужному повторному созданию многих экземпляров компонентов и узлов DOM, что может привести к снижению производительности и потере состояния в дочерних компонентах.
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://17.reactjs.org/docs/reconciliation.html