В сети много статей и докладов, которые описывают React Fiber, но, к сожалению, они сейчас уже не актуальны. Заметив это, я решил разобраться и актуализировать информацию. Моими основными помощниками были исходники и отладчик, поэтому здесь вы увидите множество ссылок на код из репозитория React. Теперь я хочу поделиться результатами своей работы с вами. Тема разделена на две статьи. Первая статья расскажет о процессе обновления и внесения изменений в DOM. Вторая статья посвящена реализации не блокирующего рендеринга - Concurrent React и доступна по ссылке. Данная статья является первой из двух. Хочу заметить, что первая статья, возможно, не принесет значительной практической пользы, однако описанный в ней материал рекомендуем для понимания реализации Concurrent React. Эта статья будет особенно полезна для тех, кто хочет разобраться в работе инструмента изнутри, а также извлечь интересные идеи по написанию и структурированию кода. React Fiber решает две основные задачи: Инкрементальный процесс рендеринга — способность разделять работу рендера на части. Здесь концепция и реализация во многом не поменялись, хотя и претерпели некоторые изменения. Возможность не блокирующего рендеринга, при котором рендер не должен блокировать взаимодействия пользователя с сайтом и отображение анимации. Здесь все наоборот: концепция долго разрабатывалась и видоизменялась. Подробности о том, каким образом была решена эта задача, я раскрываю в следующей статье. Важно заметить, что реализация первого аспекта, инкрементального рендеринга, является базой для реализации второго. Под рендером следует понимать процесс обновления компонентов и вычисления изменений между предыдущим и текущим рендерами для последующего внесения изменений в DOM. Далее мы разберем, как команда React решила первую задачу. В этой статье рассмотрим процесс инкрементального рендеринга, но разберем его в рамках всей работы по обновлению: обновление компонентов и внесение изменений в DOM.  Вся работа по обновлению делится на 2 фазы: Фаза render, которую иногда также называют reconciliation. В этой фазе React применяет обновления к компонентам, сравнивает предыдущее состояние приложения с текущим и выясняет, какую работу по обновлению нужно выполнить. Фаза commit. Используя наработки из предыдущей фазы, вызываются методы жизненного цикла и хуки, а также обновляется DOM. Для объяснения фаз мне потребуется вспомогательный пример. Я буду использовать данное приложение, реализующее обычный счетчик: Мы не будем начинать разбор этой фазы с самого начала. Вместо этого предлагаю сначала освоить некоторые базовые понятия процесса рендеринга, чтобы в дальнейшем повествование было более плавным и последовательным. Если представить наш пример без использования JSX, то оно будет выглядеть вот так: Можно заметить, что каждый компонент создаст и вернет React Element. В результате работы React над нашим приложением мы получи 6 React Elements: ClickCounter, div, Button, button, CounterResult, span. Каждый является обычным объектом примерно такого вида: Каждый React Element несет в себе информацию о текущем состоянии каждого элемента приложения. Fiber Node - это основной элемент в архитектуре React Fiber. Fiber Node создается из React Element с помощью функции createFiberFromElement (это не единственная функция, которая создает Fiber Node, но именно та, которая используется в нашем случае). Но сам инстанс Fiber Node создается глубже в функции createFiber. Вот как оно выглядит: Давайте рассмотрим структуру Fiber Node. Вот ее основные поля, которые помогут нам в дальнейшем (react/packages/react-reconciler/src/ReactInternalTypes.js): Используя поля return, child и sibling, мы можем сформировать Fiber Tree. Fiber Tree отображает структуру и состояние всего нашего приложения. В случае нашего примера он будет выглядеть следующим образом: React создает объект fiber root для каждого такого дерева. В fiber root хранится ссылка на Fiber Tree. Fiber Tree начинается со специального типа узла - Root и выступает родителем для вашего начального компонента Но у нас будет не одно, а целых два таких дерева: currentTree и workInProgressTree. currentTree отражает текущее, видимое состояние нашего приложения, в то время как workInProgressTree отражает новое состояние, которое необходимо применить. В ходе фазы render, workInProgressTree и его узлы будут создаваться и изменяться. Узлы workInProgressTree формируются на основе узлов currentTree с помощью функции createWorkInProgress. В этом процессе создается Fiber Node для workInProgressTree с помощью нам уже известной функции createFiber. Теперь мы можем приступить к разбору того, как происходит инкрементальный рендеринг. Суть его в следующем: мы проходимся по каждому узлу workInProgressTree и выполняем некую работу над ним. Таким образом, рендеринг происходит постепенно, обрабатывая каждый узел и имея возможность прервать работу перед обработкой следующего узла. Все начинается с создания узла Root для workInProgressTree на основе Root из currentTree. Здесь мы впервые благодарим себя за проделанную подготовительную работу, потому что мы уже знаем, что создание Root узла происходит с помощью функции createWorkInProgress. В случае первого рендера нашего приложения узел Root в currentTree будет совершенно пустым. Далее мы запускаем цикл прохода по узлам workInProgressTree. На данный момент у нас есть только один такой узел - Root, и мы начинаем с него. В данном цикле мы вызываем функцию performUnitOfWork до тех пор, пока у нас есть узлы workInProgress, которые необходимо обработать. Функция performUnitOfWork выполняет всю работу над Fiber Node, связанную с обновлением компонентов и вычислением изменений между предыдущим рендером и текущим для последующего внесения изменений в DOM. Выполнение этой функции можно разбить на два этапа: beginWork и completeUnitOfWork. Мы подробно разберем эти этапы позже, но сейчас хочу рассказать о том, что общего между ними. Обе функции заполняют значения полей flags и subtreeFlags, которые присутствуют в Fiber Node. flags и subtreeFlags определяют различные состояния и действия, которые должны быть выполнены в контексте этого узла. Все возможные значения этих полей хранятся в файле react/packages/react-reconciler/src/ReactFiberFlags.js. Примеры: Update: Указывает, что Fiber Node требует обновления. Snapshot: Указывает, что необходимо вызвать хук getSnapshotBeforeUpdate. Также стоит уточнить, что эти поля хранят двоичные значения. Так, Update = 100, а Snapshot = 10000000000. Вероятно, вы могли слышать про effects и effects list, но предполагаю, что эта теория устарела, так как я не нашел, чтобы на этапе render как-то формировался effects list. Да, у Fiber есть поля nextEffect, firstEffect, lastEffect, но они не заполняются и не используются в фазе render, а только в фазе commit. Так что утверждение, что в фазе render формируется effects list, по которому в фазе commit мы ходим и выполняем какие то действия с узлом, не верно. Функция beginWork выполняет вызов компонента и создает Fiber Nodes для дочерних узлов. Под вызовом компонента я подразумеваю процесс выполнения тела компонента. В классовых компонентах этот процесс начинается с вызова метода render(). В функциональных компонентах он представляет собой выполнение самой функции компонента. В нашем случае вызов будет выполнен для узлов ClickCounter, Button и CounterResult с помощью функции renderWithHooks. На выходе мы получаем дочерние React Elements. Далее происходит создание Fiber Node для дочерних элементов. Fiber Nodes создаются на основе React Elements. React Elements мы создали в момент вызова компонента и с помощью функции React.createElement. Я не зря привел пример нашего приложения без JSX, чтобы акцентировать внимание на том, что в return выполняется создание React Element. Если мы создаем Fiber Node впервые (то есть для текущей Fiber Node нет соответствующего узла в currentTree), то создание происходит с помощью функции createFiberFromElement. Если же мы обновляем Fiber Node (для текущей Fiber Node есть соответствующий узел в currentTree), то создается узел с помощью функции createWorkInProgress на основе узла из currentTree и созданного React Element. Процесс можно обобщить следующим образом: С помощью функции renderWithHooks выполняется вызов компонента. В процессе этого вызова создаются дочерние ReactElements. Затем на основе React Elements и узлов из currentTree создаются дочерние Fiber Nodes. Эти два этапа обеспечивают последовательное построение workInProgressTree. Кроме того, в процессе выполнения функции мы устанавливаем значения в поле flags. Используя побитовое ИЛИ: flags |= Snapshot, мы можем сохранять в поле flags несколько значений для данного узла. В конце работы beginWork наше дерево будет выглядеть так: Значение 1 обозначает PerformedWork - над узлом была выполнена работа. Данный флаг проставляется для классовых или функциональных компонент. Функция completeUnitOfWork активируется не всегда, а только в случае, когда next === null. Здесь next - это следующий дочерний узел. Таким образом, условие next === null говорит нам о том, что у текущего узла нет ни одного дочернего элемента, который требуется обработать. В этом случае срабатывает функция completeUnitOfWork. Данная функция выполняет процедуру всплытия до ближайшего узла, у которого есть непроверенный соседний узел (sibling). Во время всплытия мы задаем значение для поля subtreeFlags следующим образом: Это поле содержит флаги всех его потомков. Благодаря этому, просмотрев родительский узел, мы можем понять, какие состояния и изменения хранят его потомки. Кроме того, в ходе выполнения completeUnitOfWork определяются изменения, которые следует внести в узел. Модифицированный DOM-элемент помещается в поле stateNode, а узел помечается флагом Update. Далее я приведу пример дерева, полученного в процессе выполнения completeUnitOfWork. Однако этот пример будет описывать ситуацию, в которой была нажата кнопка, и значение счетчика изменилось с 0 на 1. Первый раз всплытие начнется с узла button, так как у него нет дочерних узлов, которые надо обработать. Мы всплывем до узла Button, у которого есть необработанный sibling узел - CounterResult. Узел span имеет flags = 100, что обозначает Update. Таким образом, в конце прохода всего дерева узел Root знает о том, что среди его потомков есть узлы с флагами PerformedWork и Update.На этом разбор фазы render мы заканчиваем и переходим к фазе commit. В этой фазе мы уже работаем с деревом finishedWork, которое является workInProgressTree из фазы render: На стадии commit, используя построенное дерево и опираясь на поля flags и subtreeFlags, мы выполняем методы жизненного цикла, хуки и обновляем DOM. Фаза commit начинается с функции commitRoot, но основная логика находится в commitRootImpl. В этой функции мы рассмотрим четыре основных подфункции, которые вызываются в следующем порядке: commitBeforeMutationEffects - выполняет действия перед обновлением DOM. Например getSnapshotBeforeUpdate. commitMutationEffects - выполняет изменения DOM; отрисовка происходит позже, когда у браузера будет доступное время. commitLayoutEffects - выполняет useLayoutEffect. flushPassiveEffects - выполняет useEffect. Теперь давайте разберемся, как поля flags и subtreeFlags помогают нам. Допустим, у нас в subtreeFlags есть значение 10000000000 - это флаг Snapshot. Перед выполнением commitBeforeMutationEffects мы можем посмотреть на subtreeFlags у узла Root и понять, есть ли у потомков узел, на котором необходимо выполнить getSnapshotBeforeUpdate. Эта проверка производится с помощью побитового И: subtreeFlags & Snapshot. Таким образом, мы можем постепенно спускаться по дереву и в конечном итоге найти нужный нам узел. То же самое применимо и для нашего примера. В случае выполнения commitMutationEffects нам необходимо внести изменения в DOM. Как нам найти узлы, которые нужно обновить? Мы проверяем условие subtreeFlags & Update, чтобы понять, есть ли у его потомков узлы, которые необходимо обновить. Таким образом, мы спускаемся по всему дереву и доходим до нашего узла span. Мы с вами разобрали работу инкрементального рендеринга, который, разбивая рендеринг на части, помогает реализовать Concurrency. Таким образом, можем выполнять рендеринг не весь за раз, а по частям, постоянно анализирую, нет ли срочных обновлений, которые необходимо отрендерить, а также давая возможность браузеру выполнить отрисовку, не блокируя при этом его. Но подробнее об этом в следующей статье.