Привет, Хабр! Предлагаю вашему вниманию перевод статьи "React Fiber Architecture" автора Andrew Clark.
Вступление
React Fiber — прогрессивная реализация ключевого алгоритма React. Это кульминационное достижение двухгодичных исследований команды разработчиков React.
Цель Fiber в увеличении производительности при разработке таких задач как анимация, организация элементов на странице и движение элементов. Ее главная особенность это инкрементный рендеринг: способность разделять работу рендера на единицы и распределять их между множественными фреймами.
Другие ключевые фичи включают возможность приостановки, отмены или переиспользования входящих обновлений DOM дерева, возможность приоритизации разных типов обновлений, а также — согласование примитивов.
Перед прочтением данной статьи рекомендуется ознакомиться с основными принципами React:
- React Components, Elements, and Instances
- Reconciliation
- React — Basic Theoretical Concepts
- Design Principles
Обзор
Что такое сверка (reconciliation)?
Сверка — это алгоритм React, используемый для того, чтобы отличить одно дерево елементов от другого для определения частей, которые нужно будет заменить.
Апдейт — это изменение в данных, которые используются для отрисовки React приложения. Обычно это результат вызова метода setState; конечный результат отрисовки компонента.
Ключевая идея React API — мыслить об апдейтах так, если бы они могли привести к полной отрисовке приложения. Это позволяет разработчику действовать декларативно, а не переживать о том насколько рациональным будет переход приложения из одного состояния в другое (от А до B, B до С, С до A и тд.).
В целом, отрисовка всего приложения на каждое изменение работает только в наиболее традиционных приложениях. В реальном мире это негативно сказывается на производительности. Реакт влючает в себя оптимизации, которые создают представление полной отрисовки, не затрагивая огромную долю производительности. Большую часть этих оптимизаций и включает в себя процесс, называемый сверка.
Сверка — это алгоритм, за которым стоит то, что мы привыкли называть «Virtual DOM». Определение звучит как-то так: когда вы рендерите React приложение, дерево елементов, которое описывает приложение генерируется в зарезервированной памяти. Это дерево потом включается в рендеринг окружение — на примере браузерного приложения, оно переводится в набор DOM операций. Когда состояние приложения обновляется (обычно вызовом setState), новое дерево генерируется. Новое дерево сравнивается с предыдущим, чтоб просчитать и включить именно те операции, которые нужны для перерисовки обновленного приложения.
Несмотря на то что Fiber это близкая реализация сверщика, высокоуровненый алгоритм, обьясненный в React документации будет в большинстве таким же.
Ключевые понятия:
- Разные типы компонентов предполагают генерацию существенно разных деревьев. React не будет пытаться сравнить их, а просто заменит старое дерево полностью .
- Различие списков производиться с использованием ключей (keys). Ключи должны быть «постоянными, предсказуемыми и уникальными».
Сверка против рендеринга
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"), где вычисления могут быть отменены когда это необходимо.
Реакт это библиотека не для обработки обобщенных данных. Это библиотека для построения пользовательских интерфейсов. Мы думаем что у него должна быть уникальная позиция в приложении, чтоб определять какие вычисления являются подходящими, а какие нет в данный момент.
Если что-либо за кулисами, значит мы можем отменить всю логику связанную с этим. Если данные приходят быстрее чем норма отрисовки кадров, мы можем обьеденить обновления. Мы можем увеличить приоритет работы, приходящей вследствие взаимодействия с пользователем (такую как появление анимации при нажатии на кнопку) против менее важной работы на бэкграунде (отрисовка нового контента подгруженного с сервера), чтобы предотвратить скачки фреймов.
Ключевые понятия:
- В пользовательских интерфейсах не важно чтоб каждое обновление было применено сразу; фактически такое поведение будет лишним, оно будет способствовать падению фреймов и ухудшению UX.
- Разные типы апдейтов имеют разные приоритеты – обновления анимации должны заканчиваться быстрее чем, скажем, обновление данных хранилища.
- Проталкивающий (push-based) подход трубует от приложения (вас, разработчика) решать как планировать работу. Протягивающий (pull-based) подход позволяет фреймворку принимать решения за вас.
Реакт на данный момент не имеет приемущества планирования в значитильной мере; результаты обновлений всего поддерева будут отрисовываться незамедлительно. Тщательный отбор елементов в алгоритме ядра React, чтобы применить планирование – ключевая идея Fiber.
Что же такое Fiber?
Мы будем обсуждать сердце архитектуры React Fiber. Fiber — это более низкоуровневая абстракция над приложением чем разработчики привыкли считать. Если вы считаете свои попытки понять ее безнадежными, не чувствуйте себе обескураженными (вы не одни). Продолжайте искать и это в конце-концов даст свои плоды.
И так!
Мы достигли той главной цели архитектуры Fiber — позволить React воспользоваться планированием. Конкретно, нам нужно иметь возможность:
- остановить работу и вернуться к ней позже.
- приоритизировать разные типы работы.
- переиспользовать работу проделанную ранее.
- отменить работу, если она больше не нужна.
Чтобы все это сделать нам сначала потребуется разделить работу на единицы (units). В некотором смысле это и есть fiber (волокно). Волокно представляет единицу работы.
Чтобы продвинуться далее давайте вернемся к основной концепции React "компоненты как данные функций", часто выражаемые как:
v = f(d)
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 у компонента. В примере ниже:
function Parent() {
return <Child />
}
Ребенок волокна Parent соответвует Child.
Поле родственник (или сосед) применяется в тех случаях если render возвращает несколько детей (новая особенность в Fiber):
function Parent() {
return [<Child1 />, <Child2 />]
}
Дочерние волокна это односвязный список во главе которого первый дочерний элемент. Так что в этом примере, ребенок Parent это Child1, а родственники Child1 это Child2.
Вернемся к нашей аналогии с функциями, вы можете думать о дочернем волокне, как о функции вызываемой в конце (tail-called function).
Пример из Википедии:
function foo(data) {
a(data);
return b(data);
}
В этом примере tail-called function это b.
Возвращаемое значение (return)
Возвращаемое волокно — это волокно к которому должа вернуться программа после обработки текущего волокна. Это тоже самое что и вернуть адрес стек-сонтейнера.
Это так же можно считать родительским волокном.
Если волокно имеет несколько дочерних волокон, return каждого дочернего волокна возвращает волокно являющееся родителем. В примере выше, возвращаемое волокно у Child1 и Child2 это Parent.
Текущие и кэшированные свойства (pendingProps и memorizedProps)
Концептуально, свойства — это аргументы функции. Текущие свойства волокна это набор этих свойств в начале выполнения, закешированые — это набор в конце выполнения.
Когда входящие свойства ожидания равны закешированым, это значит что предыдущий вывод волокна может быть переиспользован без проделывания каких-либо вычислений.
Приоритет текущей работы (pendingWorkPriority )
Количество определяющей приоритет работы отображается волокном. Модуль уровня приоритета в React ReactPrioritylevel включает разные уровни приоритетов и что они представляют.
Начиная с исключения типа NoWork, которое равно 0, большее число определяет низший приоритет. Например, вы можете использовать следующую функцию чтоб проверить если приоритет волокна больше чем заданный уровень:
function matchesPriority(fiber, priority) {
return fiber.pendingWorkPriority !== 0 &&
fiber.pendingWorkPriority <= priority
}
Эта функция только для иллюстрации; она не часть базы React Fiber.
Планировщик использует поле приоритета чтоб найти следующую единицу работы которую можно выполнить. Этот алгоритм мы обсудим в следующей секции.
Альтернатива (или пара)
Обновление (flush) волокна — это значит отобразить его вывод на экране.
Волокно в разработке (work-in-progress) — волокно которое еще не было построено; другими словами – это стек-контейнер, который еще не был возвращен.
В любое время, сущность компонента имеет не более двух состояний для волокна которому соответствует: волокно в текущем состоянии, обновленное волокно или волокно в разработке.
Текущему волокну следует волокно разрабатываемое, а за тем, в свою очередь, волокно обновленное.
Следующее состояние волокна создается лениво с помощью функции cloneFiber. Практически всегда при создании нового обьекта, cloneFiber сделает попытку переиспользовать алтернативу (пару) волокна если она существует, минимизируя при этом затраты ресурсов.
Вам следует думать о поле пара (или альтернатива) как о детали реализации, но она всплывает так часто в документации, что не упомянуть ее было просто невозможно.
Вывод — это служебный елемент (или набор служебных элементов); ноды-листья React приложения. Они специфичны для кажого окружения отображения (например в браузере это ‘div’, ‘span’ и тд.). В JSX они обозначаються как строчные имена тегов.
Итог: Рекомендую попробовать особенности новой архитектуры React v16.0