День 4
Nikita Nikler
9 декабря 2024, 11:54

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

Fiber

Итак ,Fiber - это новый механизм работы ядра реакта ,пришедший на замену старому механизму Stack Reconciler в 16 версии. По большому счету он отвечает за рендер. В данном контексте рендер обозначает не render в браузере ,а абстрактный рендер - формирование ReactElements и связанных с ними FiberNodes ,а если точнее - формирование дерева FiberTree ,сравнение новой версии дерева со старым и вычисление изменений ,которые необходимо выполнить в следующей фазе commit (в рамках которой уже будут выполнены hooks ,lifecycle hooks и произведен непосредственно рендеринг в DOM ,если речь идет о react-dom). Фазу render еще часто обозначают как reconciliation. Исходя из этого можно выделить 2 основных фазы рендеринга в React:

  1. Фаза Rendering (Reconciliation) - вычисление работы для последующей фазы
  2. Фаза 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 вещах.

  1. React вычисляет все ReactElement поддерева элементов вызовом render у них
  2. 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.

Нравится? Расскажите друзьям!
Комментировать
Перейти к записи в ленте
Цель

Вы тоже можете
опубликовать свою
цель здесь

Мы поможем вам ее достичь!

310 000

единомышленников

инструменты

для увлекательного достижения

Присоединиться
Регистрация

Регистрация

Уже зарегистрированы?
Быстрая регистрация через соцсети
Вход на сайт

Входите.
Открыто.

Еще не зарегистрированы?
 
Войти через соцсети
Забыли пароль?