reconciliation react js что это

Reconciliation

React provides a declarative API so that you don’t have to worry about exactly what changes on every update. This makes writing applications a lot easier, but it might not be obvious how this is implemented within React. This article explains the choices we made in React’s “diffing” algorithm so that component updates are predictable while being fast enough for high-performance apps.

When you use React, at a single point in time you can think of the render() function as creating a tree of React elements. On the next state or props update, that render() function will return a different tree of React elements. React then needs to figure out how to efficiently update the UI to match the most recent tree.

There are some generic solutions to this algorithmic problem of generating the minimum number of operations to transform one tree into another. However, the state of the art algorithms have a complexity in the order of O(n 3 ) where n is the number of elements in the tree.

If we used this in React, displaying 1000 elements would require in the order of one billion comparisons. This is far too expensive. Instead, React implements a heuristic O(n) algorithm based on two assumptions:

In practice, these assumptions are valid for almost all practical use cases.

The Diffing Algorithm

When diffing two trees, React first compares the two root elements. The behavior is different depending on the types of the root elements.

Elements Of Different Types

Any components below the root will also get unmounted and have their state destroyed. For example, when diffing:

This will destroy the old Counter and remount a new one.

These methods are considered legacy and you should avoid them in new code:

DOM Elements Of The Same Type

When comparing two React DOM elements of the same type, React looks at the attributes of both, keeps the same underlying DOM node, and only updates the changed attributes. For example:

By comparing these two elements, React knows to only modify the className on the underlying DOM node.

After handling the DOM node, React then recurses on the children.

Component Elements Of The Same Type

Next, the render() method is called and the diff algorithm recurses on the previous result and the new result.

These methods are considered legacy and you should avoid them in new code:

Recursing On Children

By default, when recursing on the children of a DOM node, React just iterates over both lists of children at the same time and generates a mutation whenever there’s a difference.

For example, when adding an element at the end of the children, converting between these two trees works well:

If you implement it naively, inserting an element at the beginning has worse performance. For example, converting between these two trees works poorly:

In order to solve this issue, React supports a key attribute. When children have keys, React uses the key to match children in the original tree with children in the subsequent tree. For example, adding a key to our inefficient example above can make the tree conversion efficient:

Now React knows that the element with key ‘2014’ is the new one, and the elements with the keys ‘2015’ and ‘2016’ have just moved.

In practice, finding a key is usually not hard. The element you are going to display may already have a unique ID, so the key can just come from your data:

When that’s not the case, you can add a new ID property to your model or hash some parts of the content to generate a key. The key only has to be unique among its siblings, not globally unique.

As a last resort, you can pass an item’s index in the array as a key. This can work well if the items are never reordered, but reorders will be slow.

Reorders can also cause issues with component state when indexes are used as keys. Component instances are updated and reused based on their key. If the key is an index, moving an item changes it. As a result, component state for things like uncontrolled inputs can get mixed up and updated in unexpected ways.

It is important to remember that the reconciliation algorithm is an implementation detail. React could rerender the whole app on every action; the end result would be the same. Just to be clear, rerender in this context means calling render for all components, it doesn’t mean React will unmount and remount them. It will only apply the differences following the rules stated in the previous sections.

We are regularly refining the heuristics in order to make common use cases faster. In the current implementation, you can express the fact that a subtree has been moved amongst its siblings, but you cannot tell that it has moved somewhere else. The algorithm will rerender that full subtree.

Because React relies on heuristics, if the assumptions behind them are not met, performance will suffer.

Источник

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

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

3.8.1 Мотивация

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

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

На практике эти предположения применимы почти для всех практических случаев.

3.8.2 Алгоритм сравнения

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

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

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

React уничтожит старый пользовательский и перемонтирует новый.

3.8.2.2 DOM-элементы одинакового типа

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

Сравнивая эти два элемента, React обнаруживает, что необходимо изменить только имя класса на базовом узле DOM.

После обработки DOM-узла React всё рекурсивно повторяет на дочерних элементах.

3.8.2.3 Элементы компонентов одинакового типа

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

3.8.2.4 Рекурсивный обход дочерних элементов

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

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

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

3.8.2.5 Ключи

Например, добавление ключа к нашему неэффективному примеру выше может сделать преобразование дерева эффективным:

Теперь React знает, что элемент с ключом “0” является новым, а элементы с ключами “1” и “2” только что переместились.

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

Если это не так, вы можете добавить новое свойство идентификатора к своей модели или хеш части содержимого, чтобы сгенерировать ключ. Ключ должен быть уникальным среди своих соседей, а не глобально уникальным.

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

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

3.8.2.6 Компромиссы

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

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

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

Источник

Reconciliation

React provides a declarative API so that you don’t have to worry about exactly what changes on every update. This makes writing applications a lot easier, but it might not be obvious how this is implemented within React. This article explains the choices we made in React’s “diffing” algorithm so that component updates are predictable while being fast enough for high-performance apps.

When you use React, at a single point in time you can think of the render() function as creating a tree of React elements. On the next state or props update, that render() function will return a different tree of React elements. React then needs to figure out how to efficiently update the UI to match the most recent tree.

There are some generic solutions to this algorithmic problem of generating the minimum number of operations to transform one tree into another. However, the state of the art algorithms have a complexity in the order of O(n 3 ) where n is the number of elements in the tree.

If we used this in React, displaying 1000 elements would require in the order of one billion comparisons. This is far too expensive. Instead, React implements a heuristic O(n) algorithm based on two assumptions:

In practice, these assumptions are valid for almost all practical use cases.

The Diffing Algorithm

When diffing two trees, React first compares the two root elements. The behavior is different depending on the types of the root elements.

Elements Of Different Types

Any components below the root will also get unmounted and have their state destroyed. For example, when diffing:

This will destroy the old Counter and remount a new one.

DOM Elements Of The Same Type

When comparing two React DOM elements of the same type, React looks at the attributes of both, keeps the same underlying DOM node, and only updates the changed attributes. For example:

By comparing these two elements, React knows to only modify the className on the underlying DOM node.

After handling the DOM node, React then recurses on the children.

Component Elements Of The Same Type

When a component updates, the instance stays the same, so that state is maintained across renders. React updates the props of the underlying component instance to match the new element, and calls componentWillReceiveProps() and componentWillUpdate() on the underlying instance.

Next, the render() method is called and the diff algorithm recurses on the previous result and the new result.

Recursing On Children

By default, when recursing on the children of a DOM node, React just iterates over both lists of children at the same time and generates a mutation whenever there’s a difference.

For example, when adding an element at the end of the children, converting between these two trees works well:

If you implement it naively, inserting an element at the beginning has worse performance. For example, converting between these two trees works poorly:

In order to solve this issue, React supports a key attribute. When children have keys, React uses the key to match children in the original tree with children in the subsequent tree. For example, adding a key to our inefficient example above can make the tree conversion efficient:

Now React knows that the element with key ‘2014’ is the new one, and the elements with the keys ‘2015’ and ‘2016’ have just moved.

In practice, finding a key is usually not hard. The element you are going to display may already have a unique ID, so the key can just come from your data:

When that’s not the case, you can add a new ID property to your model or hash some parts of the content to generate a key. The key only has to be unique among its siblings, not globally unique.

As a last resort, you can pass an item’s index in the array as a key. This can work well if the items are never reordered, but reorders will be slow.

Reorders can also cause issues with component state when indexes are used as keys. Component instances are updated and reused based on their key. If the key is an index, moving an item changes it. As a result, component state for things like uncontrolled inputs can get mixed up and updated in unexpected ways.

Here is an example of the issues that can be caused by using indexes as keys on CodePen, and here is an updated version of the same example showing how not using indexes as keys will fix these reordering, sorting, and prepending issues.

It is important to remember that the reconciliation algorithm is an implementation detail. React could rerender the whole app on every action; the end result would be the same. Just to be clear, rerender in this context means calling render for all components, it doesn’t mean React will unmount and remount them. It will only apply the differences following the rules stated in the previous sections.

We are regularly refining the heuristics in order to make common use cases faster. In the current implementation, you can express the fact that a subtree has been moved amongst its siblings, but you cannot tell that it has moved somewhere else. The algorithm will rerender that full subtree.

Because React relies on heuristics, if the assumptions behind them are not met, performance will suffer.

The algorithm will not try to match subtrees of different component types. If you see yourself alternating between two component types with very similar output, you may want to make it the same type. In practice, we haven’t found this to be an issue.

Источник

Вступление в архитектуру React Fiber

Привет, Хабр! Предлагаю вашему вниманию перевод статьи «React Fiber Architecture» автора Andrew Clark.

Вступление

React Fiber — прогрессивная реализация ключевого алгоритма React. Это кульминационное достижение двухгодичных исследований команды разработчиков React.

Цель Fiber в увеличении производительности при разработке таких задач как анимация, организация элементов на странице и движение элементов. Ее главная особенность это инкрементный рендеринг: способность разделять работу рендера на единицы и распределять их между множественными фреймами.

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

Перед прочтением данной статьи рекомендуется ознакомиться с основными принципами React:

Обзор

Что такое сверка (reconciliation)?

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

Апдейт — это изменение в данных, которые используются для отрисовки React приложения. Обычно это результат вызова метода setState; конечный результат отрисовки компонента.

Ключевая идея React API — мыслить об апдейтах так, если бы они могли привести к полной отрисовке приложения. Это позволяет разработчику действовать декларативно, а не переживать о том насколько рациональным будет переход приложения из одного состояния в другое (от А до B, B до С, С до A и тд.).

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

Сверка — это алгоритм, за которым стоит то, что мы привыкли называть «Virtual DOM». Определение звучит как-то так: когда вы рендерите React приложение, дерево елементов, которое описывает приложение генерируется в зарезервированной памяти. Это дерево потом включается в рендеринг окружение — на примере браузерного приложения, оно переводится в набор DOM операций. Когда состояние приложения обновляется (обычно вызовом setState), новое дерево генерируется. Новое дерево сравнивается с предыдущим, чтоб просчитать и включить именно те операции, которые нужны для перерисовки обновленного приложения.

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

Ключевые понятия:

Сверка против рендеринга

DOM дерево это одно из окружений, которые React может отрисовать, к остальным можно отнести нативные iOS и Android Views с помощью React Native (Вот почему Virtual Dom — название немного неподходящее).

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

Это разделение означает, что React DOM и React Native могут использовать свои собственные механизмы рендеринга при использовании одного и того же cверщика, который находится в React Core.

Fiber – переделанная реализация алгоритма reconciliation. Она имеет непрямое отношение к рендерингу, в то время как механизмы рендеринга (отрисовщики) могут быть изменены чтоб поддерживать все приемущества новой архитектуры.

Планирование — это процесс, который определяет когда работа должна быть выполнена.

Работа — любые вычисления, которые должны быть выполнены. Работа – это обычно результат апдейта (например вызов setState).

Принципы архитектуры React настолько хороши, что могут быть описаны лишь этой цитатой:

В текущей реализации React проходит дерево рекурсивно и вызвает функции рендеринга на всем обновленном дереве в ходе одного тика (16 мс). Однако в будующем он сможет уметь отменять некоторые апдейты чтобы предотвратить скачки фреймов.
Это частообсуждаемая тема касательно React дизайна. Некоторые популярные библиотеки реализуют проталкивающий («push») подход, где вычисления производятся тогда, когда новые данные доступны. Однако, React придерживается подхода протягивания («pull»), где вычисления могут быть отменены когда это необходимо.
Реакт это библиотека не для обработки обобщенных данных. Это библиотека для построения пользовательских интерфейсов. Мы думаем что у него должна быть уникальная позиция в приложении, чтоб определять какие вычисления являются подходящими, а какие нет в данный момент.
Если что-либо за кулисами, значит мы можем отменить всю логику связанную с этим. Если данные приходят быстрее чем норма отрисовки кадров, мы можем обьеденить обновления. Мы можем увеличить приоритет работы, приходящей вследствие взаимодействия с пользователем (такую как появление анимации при нажатии на кнопку) против менее важной работы на бэкграунде (отрисовка нового контента подгруженного с сервера), чтобы предотвратить скачки фреймов.

Ключевые понятия:

Реакт на данный момент не имеет приемущества планирования в значитильной мере; результаты обновлений всего поддерева будут отрисовываться незамедлительно. Тщательный отбор елементов в алгоритме ядра React, чтобы применить планирование – ключевая идея Fiber.

Что же такое Fiber?

Мы будем обсуждать сердце архитектуры React Fiber. Fiber — это более низкоуровневая абстракция над приложением чем разработчики привыкли считать. Если вы считаете свои попытки понять ее безнадежными, не чувствуйте себе обескураженными (вы не одни). Продолжайте искать и это в конце-концов даст свои плоды.

Мы достигли той главной цели архитектуры Fiber — позволить React воспользоваться планированием. Конкретно, нам нужно иметь возможность:

Чтобы все это сделать нам сначала потребуется разделить работу на единицы (units). В некотором смысле это и есть fiber (волокно). Волокно представляет единицу работы.

Чтобы продвинуться далее давайте вернемся к основной концепции React «компоненты как данные функций», часто выражаемые как:

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

Способ по которому компьютеры в основном проверяют порядок выполнения программы называется стек вызова (call stack). Когда функция выполненена, новый стек-сонтейнер добавляется в стек. Этот стек-контейнер представляет собой работу проделанную функцией.

При работе с пользовательскими интерфейсами, слишком много работы выполняется сразу и это проблема, это может привести к скачкам анимации и будет выглядеть прерывисто. Более того, некоторая из этой работы может быть необязательна если она заменена наиболее новым обновлением. В этом месте сравнение между пользовательским интерфейсом и функцией расходится, потому что у компонентов более специфичная ответственность чем у функций вообще.
Новейшие браузеры и React Native реализует APIs, которые помогают решить эту проблему:
requestIdleCallback распределяет задачи так, чтоб низкоприоритезированные функции вызывались в простой период, а requestAnimationFrame распределяет задачи, чтоб высокоприоритезированные функции были вызваны в следующем кадре. Проблема в том, чтоб использовать эти APIs вам нужно разделить работу отрисовки на инкрементируемые единицы. Если вы полагаетесь только на стек вызовов, работа продолжится пока стек не будет пуст.

Не было бы прекрасно если бы мы могли настроить поведение стека вызовов чтоб оптимизировать отображение частей пользовательского интерфейса? Было бы здорово если бы мы могли прервать стек вызова, чтобы манипулировать контейнерами вручную?

Это и есть призвание React Fiber. Fiber — это новая реализация стека, подстроенная под React компоненты. Вы можете думать об одном волокне как о виртульном стек-контейнере.

Приемущество данной реализации стека в том что вы можете сохранить стек контейнеры в памяти и выполнить тогда (и где) вы хотите. Это решающее определение для достижения целей планирования.

Кроме планировния, мануальные действия со стеком раскрывают потенциал таких понятий как согласованность (concurrency ) и обработка ошибок (error boundaries).

В следующей секции мы рассмотрим структуру волокон.

Структура «волокна»

Если говорить конкретно, то «волокно» это JavaScript обьект, который содержит информацию о компоненте, его вводе и выводе.

Волокно согласовано со стек-контейнером, но также оно согласовано с сущностью компонета.

Вот несколько важных свойств присущих «волокну» (Этот список не исчерпывающий):

Тип и ключ

Тип и ключ служат волокну так же как и React элементы. Фактически, когда волокно создается, эти два поля копируются ему напрямую.

Тип волокна описывает компонент, которому оно соответствует. Для композиции компонентов, тип это функция или класс компонента. Для служебных компонентов (div, span) тип — это строка.

Концептуально, тип – это функция, выполнение которой прослеживается стек-контейнером.

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

Ребенок и родственник (child and sibling)

Эти поля указывают на другие волокна, описывая рекурсивную структуру волокон.

Ребенок волокна соответсвует значению, которое было возвращено вследствие вызова метода render у компонента. В примере ниже:

Ребенок волокна Parent соответвует Child.

Поле родственник (или сосед) применяется в тех случаях если render возвращает несколько детей (новая особенность в Fiber):

Дочерние волокна это односвязный список во главе которого первый дочерний элемент. Так что в этом примере, ребенок Parent это Child1, а родственники Child1 это Child2.

Вернемся к нашей аналогии с функциями, вы можете думать о дочернем волокне, как о функции вызываемой в конце (tail-called function).

Пример из Википедии:

В этом примере tail-called function это b.

Возвращаемое значение (return)

Возвращаемое волокно — это волокно к которому должа вернуться программа после обработки текущего волокна. Это тоже самое что и вернуть адрес стек-сонтейнера.
Это так же можно считать родительским волокном.

Если волокно имеет несколько дочерних волокон, return каждого дочернего волокна возвращает волокно являющееся родителем. В примере выше, возвращаемое волокно у Child1 и Child2 это Parent.

Текущие и кэшированные свойства (pendingProps и memorizedProps)

Концептуально, свойства — это аргументы функции. Текущие свойства волокна это набор этих свойств в начале выполнения, закешированые — это набор в конце выполнения.

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

Приоритет текущей работы (pendingWorkPriority )

Количество определяющей приоритет работы отображается волокном. Модуль уровня приоритета в React ReactPrioritylevel включает разные уровни приоритетов и что они представляют.

Начиная с исключения типа NoWork, которое равно 0, большее число определяет низший приоритет. Например, вы можете использовать следующую функцию чтоб проверить если приоритет волокна больше чем заданный уровень:

Эта функция только для иллюстрации; она не часть базы React Fiber.

Планировщик использует поле приоритета чтоб найти следующую единицу работы которую можно выполнить. Этот алгоритм мы обсудим в следующей секции.

Альтернатива (или пара)

Обновление (flush) волокна — это значит отобразить его вывод на экране.

Волокно в разработке (work-in-progress) — волокно которое еще не было построено; другими словами – это стек-контейнер, который еще не был возвращен.

В любое время, сущность компонента имеет не более двух состояний для волокна которому соответствует: волокно в текущем состоянии, обновленное волокно или волокно в разработке.

Текущему волокну следует волокно разрабатываемое, а за тем, в свою очередь, волокно обновленное.

Следующее состояние волокна создается лениво с помощью функции cloneFiber. Практически всегда при создании нового обьекта, cloneFiber сделает попытку переиспользовать алтернативу (пару) волокна если она существует, минимизируя при этом затраты ресурсов.

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

Вывод — это служебный елемент (или набор служебных элементов); ноды-листья React приложения. Они специфичны для кажого окружения отображения (например в браузере это ‘div’, ‘span’ и тд.). В JSX они обозначаються как строчные имена тегов.

Итог: Рекомендую попробовать особенности новой архитектуры React v16.0

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *