Как создать сайтТермины Статьи Последние правки
20.04.2018
Unity3D

Статьи / JavaScript / Reactjs (документация, руководство, примеры, flux) / Справочник (Reference)


Согласование (Reconciliation)

Согласование


React ключевое дизайнерское решение, которое предоставляет API, кажущееся будто бы оно отрендеривает повторно все приложение на каждом обновлении. Это делает написание приложений намного проще, но и с невероятными проблемами, чтобы сделать его управляемым. Эта статья объясняет, как с помощью мощной эвристики нам удалось уменьшить сложность проблемы с O(n3) до O(n).

Мотивация


Создание минимального числа операций для преобразования одного дерева в другое, сложная и хорошо изученная проблема. Состояние алгоритмов, имеют сложность в порядке O(N3), где N есть число узлов в дереве.

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

С оптимальным неуправляемым алгоритмом, мы реализуем неоптимальной O(N) алгоритм с использованием эвристических методов на основе двух предположений:

1. Два компонента того же класса будет генерировать схожие деревья и два компоненты различных классов будет генерировать различные деревья.

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

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

Сравнение двух точек


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

Различные типы узлов

Если тип узла отличается, React будет относиться к ним как двум разным под-деревьям, отбрасываем первое и строим/вставляем второе.

renderA: <div />
renderB: <span />
=> [removeNode <div />], [insertNode <span />]

Та же логика используется и для пользовательских компонентов. Если они не одного и того же типа, React не собирается даже пытаться сопоставлять то, что они делают. Он просто удалит первый из DOM и вставит второй.
renderA: <Header />
renderB: <Content />
=> [removeNode <Header />], [insertNode <Content />]

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

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

Как следствие, если есть
элемент в том же положении, в двух последовательных рендерах, можно было бы ожидать увидеть очень похожую структуру и стоит исследовать её.

DOM Узлы

При сравнении двух узлов DOM, мы смотрим на атрибуты обоих и можем решить, какие из них изменилось в линейном времени.
renderA: <div id="before" />
renderB: <div id="after" />
=> [replaceAttribute id "after"]

Вместо того чтобы рассматривать style как непрозрачные строки, используется объект ключ-значение. Это позволяет нам обновлять только те свойства, которые изменились.
renderA: <div style={{color: 'red'}} />
renderB: <div style={{fontWeight: 'bold'}} />
=> [removeStyle color], [addStyle font-weight 'bold']

После того, как атрибуты были обновлены, мы рекурсивно проходим всех детей.

Пользовательские компоненты

Мы решили, что два пользовательских компонента одинаковы. Так как компоненты являются компонентами с сохраненным состоянием(stateful), мы не можем просто использовать новый компонент и назовать его эпохой (call it a day). React возьмет все атрибуты нового компонента и вызовет component[Will/Did]ReceiveProps() на предыдущем.

Предыдущий компонент является сейчас действительным. Его render() метод вызывается и алгоритм Diff перезапускается с новым результатом и предыдущим результатом.

Сравнение списков


Проблемные Случаи

Для того, чтобы сделать согласование детей, React принимает очень простой подход. Он пробегает по обоим спискам детей, в то же время и определяет изменения, когда есть разница.

Например, если вы добавляете элемент в конец:
renderA: <div><span>first</span></div>
renderB: <div><span>first</span><span>second</span></div>
=> [insertNode <span>second</span>]

Вставка элемента в начало проблематична. React будет видеть, что оба узла span-ы и поэтому будет работать в режиме изменений.
renderA: <div><span>first</span></div>
renderB: <div><span>second</span><span>first</span></div>
=> [replaceAttribute textContent 'second'], [insertNode <span>first</span>]

Есть много алгоритмов, которые пытаются найти минимальные наборы операций, для преобразования списка элементов. Расстояние Левенштейна может найти минимум, используя один элемент вставки, удаления и замены в O(N2). Даже если бы мы использовали Левенштейн, то не смогли найти, когда узел перемещается в другое положение и алгоритмов, чтобы сделать это при гораздо большей сложности.

Ключи

Для того чтобы решить эту проблему, кажущуюся неустранимой, был введен дополнительный атрибут. Вы можете добавить каждому ребенку ключ, который будет использоваться, чтобы произвести согласование. Если вы укажете ключ, React станет способен найти вставки, удаления, замены и движется в O(N), используя хэш-таблицу.
renderA: <div><span key="first">first</span></div>
renderB: <div><span key="second">second</span><span key="first">first</span></div>
=> [insertNode <span>second</span>]

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

Компромиссы

Важно помнить, что у алгоритма согласования есть детали реализации. React может вновь выводить(рендерить) всё приложение на каждом действии, конечный результат будет тот же. Мы регулярно корректируем эвристики для того, чтобы сделать распространенные сценарии использования быстрее.

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

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

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

2. Если вы не обеспечите стабильные ключи (с помощью Math.random(), например), все под-деревья будут повторно отрендерены каждый раз. Если дать пользователям возможности выбирать ключ, то они будут иметь возможность выстрелить себя в ногу.