Что такое React Fiber - React Fiber Architecture
Tags: react, fiber
Published: 2022-12-29 13:25:50

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

*Перевод статьи Эндрю Кларка “React Fiber Architecture” [https://github.com/acdlite/react-fiber-architecture](https://github.com/acdlite/react-fiber-architecture)*

### Введение

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

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

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

### Об этом документе

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

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

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

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

Цель автора — сформировать понимание архитектуры Fiber достаточное для того, чтобы ориентироваться в его изменениях по мере развития, и, в конечном счете, чтобы быть способным внести свои изменения в React.

### Перед тем как начать…

Автор настойчиво рекомендует, чтобы читатель для начала ознакомился со следующими статьями:

- [React Components, Elements, and Instances](https://facebook.github.io/react/blog/2015/12/18/react-components-elements-and-instances.html). Компонент — часто перегруженный термин, важно уверенно понимать, что он означает.
- [Reconciliation](https://facebook.github.io/react/docs/reconciliation.html). Высокоуровневое описание алгоритма реконсилиации.
- [React Basic Theoretical Concepts](https://github.com/reactjs/react-basic). Описание концептуальной модели React неотягощенная реализацией. Что-то из этого может быть непонятно при первом чтение — это нормально, станет понятней позже.
- [React Design Principles](https://facebook.github.io/react/contributing/design-principles.html). Обратите особое внимание на раздел про планирование — в нем даны хорошие объяснения причинам, стоящим за React Fiber.

### Обзор

Пожалуйста, ознакомьтесь с разделом “Перед тем как начать”, если вы этого еще не сделали.

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

### Что такое реконсилиация?

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

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

Центральная идея API React-а — считать что обновления вызывают переотрисовку всего приложения. Это позволяет разработчикам рассуждать декларативно, вместо того, чтобы беспокоиться о том, как эффективно перевести приложение из одного состояния в другое.

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

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

Хотя Fiber это попытка переписать с нуля реконсилятор, высокоуровневое описание [алгоритма в документации React](https://reactjs.org/docs/reconciliation.html) будет почти такое же. Ключевые моменты такие:

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

### Реконсилиация vs Отрисовка

DOM браузера это только одна из сред отрисовки в которой может работать React, отрисовка в других средах, iOS и Android, достигается средствами React Native. По этой причине термин “виртуальный *DOM*” немного некорректен.

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

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

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

### Планирование

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

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

[Принципы Архитектуры](https://reactjs.org/docs/design-principles.html) React задокументировали это настолько хорошо, что можно привести цитату:

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

Ключевые вещи:

- В пользовательском интерфейсе нет необходимости применять каждое обновление сразу — это может быть нерациональным и быть причиной для потери частоты кадров и ухудшения пользовательского опыта.
- У разных типов обновлений, разные приоритеты. Обновление связанное с анимацией должно быть выполнено быстрее, чем, скажем, обновление из хранилища данных.
- Push-подход требует от приложения (от разработчика) решить, как спланировать работу. Pull-подход позволяет фреймворку (React) быть умнее и принимать такие решения за разработчика.

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

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

### Что такое файбер?

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

Поехали!

---

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

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

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

Чтобы двигаться дальше, вернемся к концепции [“React-компонент как функция от данных”](https://github.com/reactjs/react-basic#transformation), которая обычно выражается как `v = f(d)`.

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

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

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

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

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

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

Польза от переделывания стека в том, что можно [сохранить фреймы стека в памяти](https://www.facebook.com/groups/2003630259862046/permalink/2054053404819731/) и выполнять их как и когда захочется. Это критично для достижения наших требований к планированию.

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

В следующем разделе мы подробнее рассмотрим структуру файбера.

### Структура файбера

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

Файбер соответствует фрейму стека, но также соответствует экземпляру компонента. Перечислим некоторые важные поля файбера (не все):

### `type` и `key`

`type` и `key` служат такой же цели, как и в случае React-элемента. Фактически, когда файбер создается из элемента, эти два поля просто копируются из него.

`type` ссылается на компонент соответствующий файберу. Для составных компонентов, `type` это функция или класс компонента. Для хост-компонентов (`div`, `span` и так далее), `type` это строка.

Концептуально, `type` это функция (как в `v = f(d)`), выполнение которой отслеживается стековым фреймом.

Вместе с полем `type`, поле `key` используется во время реконсилиации для определения того, можно ли переиспользовать результат файбера.

### `child` и `sibling`

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

Поле `child` (потомок) ссылается на файбер, соответствующий результату функции `render` компонента. Например:

```jsx
function Parent() {
	return <Child />
}
```

здесь, `Child` будет файбером-потомком для `Parent`.

Поле `sibling` (элемент того же уровня) учитывает случай, когда `render` возвращает несколько потомков:

```jsx
function Parent() {
	return [<Child1 />, <Child2 />]
}
```

Поле `child` ссылается на первый файбер в односвязном списке потомков. В примере выше, `Child1` является файбером-потомком для `Parent`, а `Child2` - одноуровневый элемент (`sibling`) для `Child1`.

Возвращаясь к аналогии с функциями, файбер-потомок можно представлять как [хвостовую рекурсию](https://en.wikipedia.org/wiki/Tail_call).

### `return`

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

Если у файбера есть несколько потомков, `return` каждого потомка ссылается на родителя. В примере выше, для `Child1` и `Child2` `return` будет ссылаться на `Parent`.

### `pendingProps` и `memoizedProps`

Концептуально, пропсы (props) это аргументы функции. Файберу устанавливаются `pendingProps` в начале выполнения, а `memoizedProps` устанавливаются в конце.

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

### `pendingWorkPriority`

Число означающее приоритет работы соответсвующего файбера. В модуле ReactPriorityLevel перечислены разные уровни приоритетов и то, что они представляют.

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

```jsx
function matchesPriority(fiber, priority) {
	return fiber.pendingWorkPriority !== 0 &&
		fiber.pendingWorkPriority <= priority
} 
```

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

### `alternate`, дублер

*очистка файбера*

Очистить файбер означает отрисовать его результат на экране.

*незавершенный файбер*

Необработанный файбер. Концептуально, текущий фрейм стека.

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

Дублер (`alternate`) данного файбера — незавершенный файбер, а дублер незавершенного файбера — данный файбер.

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

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

### `output`, вывод

*хост-компонент*

Хост-компонент — листовая вершина в дереве элементов приложения. Хост-компоненты специфичны для среды отрисовки (например, в браузере это `div`, `span` и так далее). В JSX, они обозначаются с помощью тегов в нижнем регистре.

Концептуально, вывод файбера аналогичен результирующему значению функции.

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

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

### Будущие разделы

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

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

### Видео по теме

[What's Next for React (ReactNext 2016)](https://youtu.be/aV1271hd9ew)