Вывод

Эту цель продолжу чуть позже немного в другом формате
Дневник цели
Комментарии

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

Ого ,какие люди) Спасибо за совет! У меня к сожалению очень мало коммерческого опыта на реакт ,и нужно чем-то выделиться среди конкурентов. Рассчитываю выехать за счет хорошего понимания важных для бигтехов вещей типа хайлод штук всяких и веб секьюрити ,ну и хорошим знанием реакт ,чтобы на вопрос "что такое реконсиляция" был дал четкий сеньорский ответ ,обозначающий мои серьезные намерения в получении работы у них) Но понятно ,что вопроса этого может и не быть ,и это только лишь плюс ,однако тут еще личный интерес тоже побуждает)
Я просто за все время работы никогда не интересовался внутрянками инструментов ,которые использовал. И очень поверхностно их изучал ,исключительно на практике по мере требования. В этот раз хочется изменить подход ,копнуть чуть глубже библиотеку ,понять ,почему тут надо оптимизировать ,а тут нет ,а не просто следовать этим рекомендациями. Как-то так) Реакт мне очень нравится ,после 8 лет ангуляров ,и чем глубже я погружаюсь в него ,тем интерес сильнее на самом деле. С ng такого не было =)

Попробую сразу здесь описать ошибки ,чтобы от них отталкиваться при проектировании новой схемы.
Ошибка 1. FiberRoot отвечает за current и workInProgress деревья
Это вовсе не так. Более того в этом файбере не хранятся ссылки на эти деревья. Формально есть поле current ,но немного для другого сделано. А ключевая особенность FiberRoot в том ,что именно на его уровень вынесена делегация событий React. В то время как в прошлых версиях React делегация событий была вынесена на уровень window (или document) ,что не позволяло создать на одной странице несколько рутов приложений. Ну и суть его в инкапсуляции самого приложения внутри себя.
workInProgress и current как отдельных деревьев не существует. Fiber ссылаются на свою альтернативную версию (рабочую или текущую) через поле alternate.
Ошибка 2. Создается временное дерево ReactElement ,которое используется для получения diff с Fiber
Это тоже ложь. В текущей версии реакт никакое дерево реакт-элементов (или нод) не создается ,это было актуально в старых версиях реакта ,когда создавался полноценный Virtual DOM для сравнения с предыдущим и представлял собой рекурсивные деревья. Fiber как раз решил эту проблему (проблему обхода больших деревьев рекурсией) ,представив состояние приложения в виде структуры Fiber ,которую можно обойти в одном направлении через использование указателей односвязанных списков.
Ошибка 3. Создается новый FIber если есть изменения
Fiber создается только один раз при монтировании компонента ,а далее он изменяется. Изменяется его ссылка alternate на разных этапах обработки реактом ,а также некоторые другие свойства. Подробно этот момент я разберу потом.
Ошибка 4. current сожержит слепок всего дерева Fiber
Это не так. В момент обработки реактом current считается Fiber ,на котором случились события или эффекты. Это тот узел ,от которого запустился цикл изменений ,и в основном это какая-то часть полной структуры Fiber.
Ошибка 5. Reconciliation - это фаза обновления реакта
Это не так. Reconciliation - это процесс ,который запускается в какой-то определенный момент. А фаз существует 3 - Begin -> Update -> Commit. Тут термины в различных источниках отличаются ,кто-то использует термин render. Но суть в том ,что фазы 3 ,а reconciliation не является фазой обновления.

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

Решил немного углубиться в структуру объекта Fiber. Эти знания будут нужны для понимания работы алгоритмов React.
Первое поле ,которое нас встречает в типе Fiber исходного кода реакт - tag. Оно типизировано типом WorkTag и представляет собой перечисление числовых литералов от 0 до 29.
export type WorkTag = 0 | 1 | ... | 28 | 29;
Для каждого числа из перечисления существует константа со значением соответствующего числового литерала. Например:
export const FunctionComponent = 0;
Прежде ,чем попытаться дать определение этому полю ,посмотрим на другие константы ,которое оно может принимать:
- FunctionComponent (0) - представляет функциональный компонент
- ClassComponent (1) - представляет классовый компонент
- HostRoot (3) - представляет корневой узел приложение ,то что создается через React.createRoot(). Является верхушкой дерева Fiber
- HostComponent (5) - представляет обычный DOM-элемент
- HostText (6) - представляет текстовый узел DOM
- Fragment (7) - представляет фрагмент React (<></> или React.Fragment)
- MemoComponent (14) - представляет функциональный компонент ,обернутый в memo
- LazyComponent (16) - представляет компонент ,загружаемый асинхронно
- и т.д. прочие теги пока что не очень интересны
Здесь можно заметить ,что Fiber хранит не тип ReactNode ,с которым он непосредственно связывается ,а способ создания этого ReactNode. Эту информацию он извлекает непосредственно из объекта ReactNode. Учитывая ,что ReactNode является типом-перечислением других типов ,свойства могут анализироваться разные для каждого типа.
Рассмотрим для простоты тип ReactElement ,который возвращается функциональными компонентами или методом render классовых. Он также может представляться элементом DOM ,созданным через React.createElement (или JSX-разметкой ,которая через babel все равно превратится в createElement). Нас будут интересовать 2 поля - $$typeof и type.
Сперва React определит ,что узел является ReactElement ,проверив $$typeof ,ведь для него он будет равен значению Symbol(react.element). Это позволяет React сразу же классифицировать ноду и понять ,как ее обрабатывать дальше.
Затем он посмотрит на поле type ,которое в случае ,если:
- компонент функциональный - содержит ссылку на функцию (определение компонента)
- компонент классовый - содержит ссылку на функцию-конструктор компонента
- DOM-компонент - содержит строку с названием тега
Но тут загвоздка. И Function и Class представляют собой тип function в рантайме javascript. Как React их различает? Все довольно просто ,у функции-конструктора есть prototype ,хранящий отличительные методы ,характеризующие его как класс компонента. Например ,prototype.render ,на который и ориентируется React при проверке.
Разобрались с тем ,как это поле заполняется. Кстати заполняется оно при создания нового Fiber - в процессе первичного рендеринга или на этапе реконсиляции при цикле обновлений ,если это требуется. Далее рассмотрим ,какую роль он играет в механизмах React?
На основе значения tag React выбирает подходящий алгоритм для работы с данным Fiber для дальнейшего обновления DOM. При анализе изменений ,React может сравнить tag и сразу же понять ,что например при его отличии от предыдущей версии нужно создать новый Fiber ,и анализировать другие свойства уже нет смысла ,тем самым сократив кол-во всевозможных проверок до одной и оптимизировать цикл.
Такая ситуация может произойти в результате изменения структуры элементов ,например ,при работе со списками или условным отображением. Если вдруг после рендеринга на позиции компонента <A /> вдруг встал элемент <div /> ,то именно такая ситуация и произойдет ,тк это ноды разного типа. Однако ,если для A указать key ,то он корректно сопаставится с Fiber и не будет перемонтирован. То есть key - это еще одно свойство ,на которое смотрит React при сравнении и учитывает это. Более детально key я разберу когда-нибудь потом ,однако сразу замечу ,что в структуре объекта Fiber это свойство также присутствует.
Для tag MemoComponent React ,например также пропустит ряд проверок ,речь идет о компонентах ,обернутых в memo. Это тоже важная часть оптимизации ,которую я рассмотрю когда-нибудь позже.

Еще мне пришло небольшое осознание на счет целей объекта Fiber. Наибольший интерес этот объект представляет с точки зрения предоставления информации о том ,как React должен изменить DOM. Ведь именно эти сведения хранятся в FIber рабочего дерева.
Таким образом Fiber можно считать некой рабочей единицей React ,которая представляет сведения ReactDOM о том ,что нужно сделать с DOM. Эта единица абстрагирована от реализации рендерера ,и может работать как с DOM в браузере ,так и вдругих средах - Node (для SSR) или React Native. Как я понимаю ,на фазе commit в работу вступает рендерер ReactDOM ,который умеет работать с единицей Fiber.

Итак ,я попытался нарисовать схему этапа Reconciliation и очень абстрактно этапа Commit.
Для примера я взял простую структуру приложения ,которая состоит из:
root (контейнер приложения)
-- App (компонент)
---- Button (компонент)
------ ButtonElement (HTMLButtonElement)
-------- TextNode (текстовый узел)
---- Text (компонент)
------ TextNode (текстовый узел)
Для простоты представим ,что у компонента Button вызвался setState ,что положило начало процессу реконсиляции (Reconciliation).
React знает ,что setState был вызван именно на компоненте Button ,а потому он создает временное дерево ReactElement для него и всех его дочерних узлов ,в нашем случае это сам Button, ButtonElement и TextNode. Его создает React для того ,чтобы в последствии сравнить с соответствующими Fiber из текущего дерева Fiber. Формально вызываются render у каждого такого компонента или элемента ,чтобы получить новое состояние для ReactElement на основе пропсов и стейта.
Этот процесс на схеме обозначен блоком New ReactElementTree. Мы можем заметить ,что было создано дерево не для всего приложения ,а только для элемента и его потомков ,на котором был вызван setState.
Далее React сравнивает состояния узлов из временного дерева ReactElement с состоянием ,содержащимся в Fiber для этих узлов. На данный момент сравнения происходят с Fiber из текущего дерева Fiber. Если на основе Diff состояний React находит разницу ,то создает новый Fiber для этого элемента и кладет его в рабочее дерево Fiber.
На схеме я обозначил этот процесс стрелкой create new ,соединяющую файберы из колонок current и workInProgress.
Если состояние ReactElement не потерпело изменений ,то новый Fiber для этого узла не создается ,а переиспользуется из текущего дерева Fiber. То есть передается по ссылке.
Когда дерево workInProgress или рабочее дерево Fiber было заполнено ,временное дерево ReactElement удаляется ,тк в дальнейшей работе уже не пригодится. Я обозначил это переходом в состояние оранжевого цвета. Далее на схеме этот цвет будет ассоциироваться с удалением ,зачисткой.
Итак ,когда дерево workInProgress сформировано ,в работу вступает механизм React Scheduler ,который расставляет приоритеты для задач Fiber и формирует очередь задач на выполнение. Сформированная очередь задач попадает в фазу commit ,в которой задачи выполняются в порядке выставленных приоритетов. В результате производится либо обновление DOM ,либо вызов эффектов реакта. В качестве примера я взял эффект useEffect ,который выполняется после обновления DOM ,следовательно приориет этой задачи ниже ,чем у задачи обновления DOM - я показал это на схеме.
Сейчас если перенести взгляд на верхнюю часть схемы ,можно увидеть ,что дерево current начинает смотреть на workInProgress. Это тот самый процесс ,когда FiberRoot начинает считать текущим деревом дерево workInProgress ,а workInProgress дерево удаляется.
Здесь стоит пояснить некоторые обозначения на схеме.
Элемент Root представляет собой контейнер приложения и имеет связь с FiberRoot. Объект FiberRoot содержит в себе свойства current и workInProgress ,которые представляют ссылки на соответствующие деревья Fiber. Их я также обозначил на схеме ,чтобы выразить изменение их состояния в процессе одного цикла обновлений.
Каждый элемент в дереве также имеет связь с Fiber ,это показано скругленным прямоугольником со стрелкой отношения. Элементы ,в которых произошли изменения (props или состояния) ,я пометил зеленым цветом. Fiber ,которые должны создаться заново я пометил синим цветом. Все что не имеет цвета является неподвергшимся изменениям объектом. Однако блок current под конец цикла обновлений все-таки меняется ,но я не стал его заркашивать ,чтобы не запутаться.
Завтра я буду обсуждать эту схему с ментором. На текущий момент это мое понимание процесса реконсиляции ,но оно может быть неправильным. Если здесь будут ошибки ,я опишу их в следующем посте. В дальнейшем я попробую каждый процесс разобрать подробнее. В особенности интересно понять ,где каждый из основных хуков реакт будет располагаться на этой схеме ,и как работают приоритеты задач ,а также сам цикл выполнения задач. Это знание поможет глубже понять и осознать те ли иные рекомендации по отпимизации при работе с компонентами или хуками.


Хочу внести немного ясности в терминологию Fiber ,которую использовал в размышлениях ниже.
Я использовал такие термины как FiberNode, ReactFiber, CurrentFiberTree, WorkInProgressTree и FiberTree. Все они на самом деле с точки зрения реализации представляют один объект с типом Fiber. В исходниках реакта есть несколько функций ,которые по-разному могут создавать этот Fiber ,но по итогу мы имеем дело всегда с объектом типа Fiber. Поэтому в будущем я буду корректно обзывать их Fiber без каких-либо префиксов и суффиксов ,которые могут запутать в понимании.
Дерево Fiber также представляет собой объект Fiber ,содержащий указатели на другие Fiber ,которые тоже могут быть деревьями ,а могут быть конечными узлами без детей и соседей.
Кроме этого существует такая сущность как FiberRoot. Она существует в приложении в единственном экземпляре ,содержит более расширенный набор свойств и несет немного другую функциональность.
FiberRoot создается при вызове React.createRoot и содержит ссылки на текущее дерево Fiber и рабочее (workInProgressTree). Он также содержит ссылку на контейнер приложения - элемент ,в который мы монтируем наше приложение при вызове render.
FIber же создается для конкретного ReactElement или ReactNode. В то время как Fiber управляет состоянием конкретно ReactNode ,FiberRoot управляет всем деревом приложения Fiber ,начиная от самого верхнего.
Основные функции FiberRoot:
- Планирование работы через React Scheduler
- Хранение деревьев Fiber current и workInProgress
- Переключение ссылок current и workInProgress
Можно обобщить ,что FiberRoot координирует работу Fiber деревьев.

Первая самая важная деталь внутренностей React ,как мне кажется ,это то ,как устроены механизмы рендеринга. Далее пойдут мои рассуждения относительно работы Fiber и каких-то предшествующих ему механизмов. В чем-то могу ошибиться ,тк пока что мое понимание на самом верхнем уровне ,и я буду отталкиваться от степени своего понимания на текущий момент ,в будущем внося коррективы и переосмысливая какие-то вещи.
Fiber
Итак ,Fiber - это новый механизм работы ядра реакта ,пришедший на замену старому механизму Stack Reconciler в 16 версии. По большому счету он отвечает за рендер. В данном контексте рендер обозначает не render в браузере ,а абстрактный рендер - формирование ReactElements и связанных с ними FiberNodes ,а если точнее - формирование дерева FiberTree ,сравнение новой версии дерева со старым и вычисление изменений ,которые необходимо выполнить в следующей фазе commit (в рамках которой уже будут выполнены hooks ,lifecycle hooks и произведен непосредственно рендеринг в DOM ,если речь идет о react-dom). Фазу render еще часто обозначают как reconciliation. Исходя из этого можно выделить 2 основных фазы рендеринга в React:
- Фаза Rendering (Reconciliation) - вычисление работы для последующей фазы
- Фаза Commit - осуществление работы (внесение изменений в DOM и выполнение побочных эффектов)
Ключевой аспект здесь в том ,что фаза 1 является прерываемой в отличии от фазы 2. В этом ключевое отличие от старого механизмма рендеринга. За счет этого повышается оптимизация ,становится возможна приоретизация задач ,появляется возможность отложить работу react для более приоритетных задач ,например - рендеринга браузера.
До Fiber реакту приходилось рекурсивно обходить все дерево реакт-элементов для вычисления диффа с реальным DOM ,и эта фаза была непрерываемой ,из-за чего могли быть просадки по FPS. Как мы помним ,чтобы частота была плавной 60к/c ,на один кадр рендеринга должно выделяться не более 16.6мс ,из которых как правило около 6мс резервируется на задачи браузера ,включая работу рендеринга ,а ~10 на задачи JS. Если задачи JS занимают более 10с ,вероятнее всего следующий кадр рендеринга будет пропущен. Их может быть пропущено и более ,если стек вызовов не очищается в течении длительного периода времени. Вполне возможно ситуация ,когда операции по вычислению diff могли занимать и более 32 мс ,что как правило приводит к заметным подвисаниям в браузере.
Помимо этого алгоритм обхода дерева изменился с рекурсивного на итеративный ,что в целом ускорило этот процесс и добавило новые возможности ,например ту же приостановку обхода и возобновление. У меня есть некоторые наработки по осмыслению этого процесса ,но его буду конкретно разбирать чуть позже в рамках других исследований.
Итак ,ранее в определении Fiber я упомянул дерево FiberTree. Что же оно собой представляет? По сути это структура компонентов приложения реализованная в виде дерева ,но с некоторыми нюансами. Каждый FiberNode представляет собой узел дерева ,связанный с конкретным ReactElement (это может быть элемент DOM или React-компонент классический или функциональный). FiberNode создается из состояния ReactElement и становится с ним неразрывно связан. Здесь можно сделать вывод ,что для создание ReactFiber требуется вычисленный ReactElement. Чтобы его вычислить и далее с ним работать ,необходимо вызвать render-функцию ,связанную с этим ReactElement. В случае компонента react - это вызов самой функции-компонента или вызов метода render классического компонента. В результате чего появляется понимание ,для чего React вызывает рендеры всего поддерева - чтобы вычислить текущее состояние реакт-элементов и далее связанный с ним Fiber для последующего выяснения того ,что нужно обновить и вызвать на этапе commit.
Поэтому остановимся на 2 вещах.
- React вычисляет все ReactElement поддерева элементов вызовом render у них
- React создает FiberNode для каждого ReactElement ,и формируя в итоге FiberTree
В первый раз React создаст целиком дерево React-элементов ,а так же целиком дерево FiberTree на его основе. Это будет сделано при вызове createRoot - render. В дальнейшем при обновлении компонентов (изменении props, state или контекстов) будут созданы соответствующее измененным реакт-элементам FiberNodes для последующего их сравнения с предыдущим состоянием. Иными словами сформировано новое дерево FiberTree. Но где хранится предыдущее состояния файберов?
Для того ,чтобы React мог найти изменение ,необходимо сохранять ссылку на старое (или правильно говоря текущее) дерево FiberTree. Оно будет называться CurrentFiberTree. В процессе рендера реакта формируется новое дерево WorkInProgressTree ,сравнение которых и называется процессом reconciliation. После завершения этого процесса CurrentFiberTree становится WorkInProgressTree. А ссылка на уже старый CurrentFiberTree удаляется.
Если заглянуть в определение типа Fiber ,то можно найти интересные свойства ,вносящие чуть больше понимания в то ,чем является FiberNode. Рассмотрим некоторые его свойства:
- type - функция ,класс или тег ,связанный с FiberNode (условно то ,что стало источником для FIberNode)
- stateNode - связанный узел DOM (это поределение взял из статьи ,однако сейчас в комментарии исходника написано следующее "The local state associated with this fiber." ,возможно для не DOM-элементов там что-то другое будет)
- return - формально текущий родитель с FiberTree или ссылка на ноду после возврата прохода вглубь
- child - непосредственный ребенок в FiberTree. Но почему не children? Это связано с алгоритмом обхода дерева ,который для погружения вглубь берет первого ребенка и далее осуществляет проход по соседним элементам вправо
- sibling - ссылка на соседнюю ноду справа на одном уровне дерева
- flags - инфо о действиях ,которые должны быть выполнены в контексте этого FiberNode
- subtreeFlags - тоже ,что и выше ,но для всего поддерева этой ноды
- alternate - ссылка на узел из альтернативного дерева (для currentFiberTree ссылка будет вести на тот же узел из workInProgressTree и наоборот)
Полей гораздо больше ,но этого пока хватит. Ключевой момент касающийся нового алгоритма обхода дерева -заложен в свойствах return, child и sibling ,который намекает на использование односвязанного списка для прохода уровня дочерних узлов. Поле type намекает на то ,с чем связан данный FiberNode - с dom-элементом или react-компонентом. stateNode - содержит состояние ассоциированного reactElement. На основе flags вероятно react определяет ,что конкретно нужно сделать в контексте этого FiberNode.
Я постарался обобщить все упомянутые процессы ,тк под капотом просиходит гораздо больше всего. Также не ссылался на исходный код ,хотя под созданием fiberNodes ,запуском процесса сравнения и самого цикла обхода стоят конкретные функции со своими особенностями. В дальнейшем я буду детальнее их рассматривать ,но сейчас задача в общих чертах разобрать Fiber ,получить базовое представление ,понять терминологию и общую схему процессов.
Ошибка 1:
После прочтения документации понял ,что допустил ошибку относительно инстансов. Инстанс элемента создается один раз и далее переиспользуется между рендерами. Выжимка из доки - "When a component updates, the instance stays the same, so that state is maintained across renders.". Правильно было говорить не об инстансе ,а о состоянии компонента. То есть происходит вычисление нового состояния и вызов render с этим новым состоянием. Render как я понимаю вернет новый ReactElement ,на основе которого будет вычислен новый связанный с ним Fiber.