React Junior
207 subscribers
37 photos
462 links
Изучение React с нуля
加入频道
Как React обходит Fiber-дерево?

Статья (англ.): https://jser.dev/react/2022/01/16/fiber-traversal-in-react/

Очень маленькая статья, описывающая алгоритм обхода FiberTree в React.

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

При обходе мы сначала спускаемся до самого низа ветки по цепочке child'ов, а когда дети заканчиваются, завершаем обработку текущего узла и поднимается на уровень выше (по ссылке в return). Обработку родительского узла также завершаем. Здесь мы проверяем, есть ли сиблинг, если есть, идем к нему, если нет, поднимаемся еще на уровень выше.

В конце концов мы дойдем до самого низа самой правой ветки и оттуда поднимемся к корню дерева, у которого ничего нет в свойстве return - обход окончен.

#ссылки #подкапотом #fiber
👍3
Первый рендеринг в React: общее понимание

Еще одна попытка разобраться в подкапотье механизма Fiber. На примере самого первого рендера.

Статья (англ.): https://jser.dev/2023-07-14-initial-mount/#311-updatehostcomponent

1. Создание FiberRoot

Повторю еще раз на всякий. Начинается все с того, что мы создаем FiberRoot, от которого потом будет расти все дерево приложения.


import { createRoot } from 'react-dom/client'
const $appContainer = document.getElementById('app’)
const root = createRoot($appContainer)


На данный момент наше дерево представлено корнем FiberRoot, а также одним обычным Fiber типа HostRoot - он соответствует HTML-контейнеру приложения - тегу #app.

2. Триггер и планирование обновления

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


root.render(‘<App />’)


Тут создается объект обновления для Fiber:HostRoot, внутри которого сохраняется элемент, который нужно отрендерить (<App />). Обновление помещается в очередь обновлений для Fiber:HostRoot (updateQueue).

На данном этапе обновление не происходит - оно только планируется на будущее.

Весь код до этого момента - синхронный.

3. Рабочий цикл планировщика

Переходим к циклу. У нас есть обновление, поэтому нужно создать новое Fiber-дерево. Буквально новое и прямо от корня - это новое дерево называется workInProgress.

Создаем новый Fiber:HostRoot - и начинаем цикл прямо с него.

На каждой итерации мы берем текущий Fiber-узел и - внимание! - создаем его child-узел. То есть с каждым шагом наше workInProgress-дерево растет.

Это делает функция beginWork - создает и возвращает дочерний узел, который станет текущим в следующей итерации цикла.

При создании child-узла для нового дерева обязательно учитывается, если у него уже есть текущая версия в current-дереве. Если есть, то старый узел будет обновлен, если еще нет, то узел будет создан заново из React-элемента.

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

Как именно создается дочерний узел, зависит от типа текущего узла. Для HostRoot данные для рендера (дочерние элементы) достаются из того самого объекта обновления, который был создан и запланирован в самом начале. Для других узлов в основном берутся из пропсов (props.children).

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

На этом «обратном» пути для каждого узла создается соответствующий DOM-элемент, который пока просто сохраняется в поле stateNode, а потом будет вставлен в реальный DOM.

Также во время спуска и подъема проставляются разнообразные флаги, которые затем будут использоваться для обновления DOM (например, что узел нужно удалить или наоборот вставить в документ).

4. Коммит изменений в DOM

Итак, мы обошли все дерево, и теперь у нас есть полностью построенное дерево workInProgress, где для каждого узла проставлены флаги, что с ним нужно делать (например, вставить в DOM или удалить).

Теперь мы реально будем менять DOM - сначала удалять, потом вставлять и менять порядок, потом запускать эффекты.

Дерево workInProgress теперь становится деревом current.

#ссылки #fiber #подкапотом
👍4
beginWork (часть 1)

Итак, самое интересное происходит внутри workLoop в функции beginWork. В нее попадает текущий узел workInProgress-дерева, а также его current-версия (она хранится в свойстве fiber.alternate).


performUnitOfWork(unitOfWork: Fiber) {
current = unitOfWork.alternate
next = beginWork(current, unitOfWork, lanes)

if (next) { // если ребенок есть
workInProgress = next // дальше вниз по ветке
} else {
completeUnitOfWork(unitOfWork) // завершение узла
}


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

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

Как именно происходит это сравнение, зависит от типа узла - fiber.tag.

updateHostRoot

Самым первым в функцию beginWork попадает Fiber:HostRoot, который соответствует главному HTML-контейнеру приложения. У него всегда есть current-версия (она создается сразу при создании FiberRoot в методе createRoot`). Обработка этого узла продолжается в функции `updateHostRoot.

Нужно узнать, какие дочерние элементы должны быть в этом узле (nextChildren) - откуда взять эту информацию? Мы помним, что при вызове функции root.render было создано и запланировано обновление для HostRoot - сейчас мы можем достать его из очереди обновлений этого узла и получить нужную информацию - processUpdateQueue.

А дальше собственно реконсиляция:


reconcileChildren(current, workInProgress, nextChildren, lanes)


Основная идея - на основе узла workInProgress и его current-версии создать дочерний узел из nextChildren и вернуть его, чтобы продолжить цикл.

reconcileChildren

Если у активного узла нет current-версии (он создается впервые), то дочерний узел будет создан функцией mountChildFibres() просто на основе элемента.

Если current-версия есть, то необходимо сравнить их поддеревья - reconcileChildFibers. Тут важно понимать, что у current-версии вполне может не быть детей (fiber.child = null), но обработка все равно пойдет по этой ветке.

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

Собственно реализация процесса реконсиляции находится в функции reconcileChildFibersImpl - и зависит она от элемента, из которого создается узел.

У нас изначально это элемент <App /> - функциональный компонент, для него сработает функция reconcileSingleElement.

Так как предыдущей версии для этого узла нет, он будет создан прямо из элемента - createFiberFromElement. Во время этого процесса мы сохраняем также пропсы нового узла (`fiber.pendingProps`) - на следующей итерации мы будем создавать дочерний узел уже для него и нам потребуется props.children.

Важно: при создании нового Fiber-узла его tag=IndeterminateComponent.

#ссылки #fiber #подкапотом
👍3
beginWork (часть 2)

В следующей итерации мы работаем со свежесозданным узлом для элемента <App />. У него стоит tag=IndeterminateComponent, так как он еще ни разу не проходил обработку, поэтому внутри beginWork для него будет выбрана ветка mountIndeterminateComponent.

Функция другая, но идея та же - нужно создать дочерний узел. Для этого компонент вызывается со всеми пропсами (`renderWithHooks`) - и мы получаем его поддерево, из которого создается новый узел (тоже изначально с типом IndeterminateComponent).

Тут же происходит замена тега для активного узла - IndeterminateComponent меняется на более подходящий FunctionComponent. (свежесозданный дочерний узел остается неопределенным).

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

completeWork

Когда у текущего Fiber-узла дочернего узла нет, необходимо завершить работу над ним - completeUnitOfWork.

Тут происходит подъем с низа ветки до первого узла с сиблингом (по которому можно перейти к следующей ветке). По пути все встреченные узлы «завершаются» и для них проставляются необходимые флаги.

Для каждого узла создается соответствующий DOM-элемент (сохраняется в свойстве fiber.stateNode). Если current-версии нет, то создается с нуля, а если есть, то обновляется (updateHostComponent).

Важно: в DOM-элемент, соответствующий Fiber-узлу, вставляется все его поддерево (DOM-элементы дочерних узлов).

#ссылки #fiber #подкапотом
👍4
Коммит изменений

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

На данный момент мы имеем полностью построенное workInProgress-дерево: для каждого Fiber-узла в нем создан соответствующий DOM-элемент и проставлены все необходимые флаги.

Тут работает функция commitMutationEffects(root, finishedWork, lanes), где finishedWork - это корень (HostRoot) нового дерева.

Под капотом там довольно много всего, но основная идея - пошаговое внесение изменений:

⁃ сначала удаляется то, что нужно удалить
⁃ потом вставляется то, что нужно вставить

Возможно, позже вернусь к этому.

https://jser.dev/2023-07-14-initial-mount/#how-react-does-initial-mount-first-time-render- тут в конце статьи есть прекрасная презентация, визуально показывающая последовательность действий при первом рендеринге

#react #подкапотом #fiber
👍4
Работа Fiber-дерева наглядно

Ссылка: https://jser.pro/ddir/rie?reactVersion=18.3.1&snippetKey=hq8jm2ylzb9u8eh468

Очень крутая пошаговая демонстрация работы Fiber-механизма в React: при первом рендере и при обновлении компонента.

#ссылки #fiber #подкапотом
👍71
Как происходит ререндер в React под капотом

https://jser.dev/2023-07-18-how-react-rerenders/

Мы уже разобрали (примерно), как происходит первый рендер React-приложения. Теперь посмотрим, как происходит ререндер - как обновляется приложение при изменениях.

Основная разница в том, что при первом рендере у нас не было предыдущего состояния - не было previous-версии fiber-узлов. А теперь есть - и React будет его переиспользовать, чтобы вносить как можно меньше изменений в реальный DOM.

Итак, после первого рендера у нас есть полностью построенное current-дерево (от FiberRoot до самого маленького текстового элемента).

Триггер и разметка пути до узла

Когда что-то происходит, меняется состояние - вызывается метод setState(). Это триггер, который запускает процесс обновления.

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

У разных событий, разные приоритеты - соответственно, и lanes будут разные, например, у события click - высший приоритет (SyncLane).

Обновление дерева

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

Собственно цикл обновления запускается так же, как и при первом рендере:

- scheduleUpdateOnFiber
- ensureRootIsScheduled
- performConcurrentWorkOnRoot
- workLoopSync (так как обновление состояния синхронное)
- prepareFreshStack (создание узла для корневого элемента)
- performUnitOfWork

Внутри performUnitOfWork все то же самое:
⁃ сначала beginWork, в которой создается следующий (дочерний для текущего) узел дерева
⁃ после рендера (создания потомка) для текущего узла pendingProps превращаются memoizedProps
⁃ при достижении конца ветки - completeUnitOfWork.

В отличие от первого рендера новые узлы дерева не создаются с нуля напрямую из компонентов - React переиспользует их «предыдущую» (alternate) версию, если она есть.

Bailout

И тут мы встречаемся с концепцией bailout (катапультирование/спасение) - оптимизация, которая позволяет избежать ненужного обновления.

React сравнивает пропсы старой и новой версии узла, и если пропсы совпдают, а на узлах этой ветки не проставлены lanes, то дальше по этой ветке обновление не идет (просто клонируются предыдущие версии узлов) - attemptEarlyBailoutIfNoScheduledUpdate.

Если сам узел не изменился, но у него проставлены childLanes (то есть изменились дочерние узлы), то сам узел пропускается, а обработка ветки идет дальше.

Обновление

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

Внутри происходят уже знакомые нам действия: рендер дочерних элементов (renderWithHooks) и собственно создание дочернего fiber-узла (reconcileChildren).

Если перерендеривается компонент, то перерендеривается все его поддерево, так как pendingProps - это новый объект при каждом рендеринге, поэтому строгое сравнение с memoizedProps не проходит и bailout не происходит. (чтобы избежать этого, мы используем useMemo).

Наконец, в completeWork узлы помечаются как обновленные и создаются все необходимые DOM-элементы.

В процессе обновления для узлов проставляются нужные флаги,
⁃ Placement для новых узлов, которые нужно вставить в DOM-дерево,
⁃ ChildDeletion - удалить некоторые дочерние элементы
⁃ Update - обновить узел

Коммит изменений

Наконец, на стадии коммита обрабатываются все проставленные флаги и вносятся изменения в DOM.

⁃ commitMutationEffectsOnFiber
- recursivelyTraverseMutationEffects (сначала дети)
- commitDeletionEffects (удаления)
- commitReconciliationEffects (Insertion)
- потом Update

#подкапотом #ссылки #fiber
👍41🔥1
reconcileChildren

Итак, произошло какое-то изменение, и мы в процессе создания нового FiberTree: обходим все узлы по порядку и создаем их свежие версии.

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

Она принимает:
⁃ старую (current) и новую (workInProgress) версию текущего fiber-узла
newChild - отрендеренный компонент для этого узла (renderWithHooks)
⁃ приоритеты рендеринга (lanes).

Функция определяет, что именно у нас отрендерилось в newChild, и на основе этого выбирает нужную ветку для создания нового FiberNode:

reconcileSingleElement - один дочерний элемент (REACT_ELEMENT_TYPE)
reconcileChildrenArray - массив дочерних элементов
reconcileSingleTextNode - текст

reconcileSingleElement

Создаем fiber, если у нас только один дочерний ReactElement.

⁃ проверяем, совпадает ли тип элемента со старой версией,

если да переиспользуем старый fiber (useFiber), если нет, удаляем старую версию, точнее помечаем для удаления (deleteChild)

⁃ удаляем все остальные дочерние элементы старой версии (если они были, т. е. если раньше было несколько элементов, а сейчас остался один) - deleteRemainingChildren.

reconcileChildrenArray

Тут много оптимизаций, связанных с порядком элементов (key) и переиспользованием старых узлов.

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

Тут также проставляются флаги:
⁃ для удаленных старых элементов (deleteChild)
⁃ для вставки новых элементов (placeChild)

Удаленные узлы

Важно: удаленные «старые» версии узлов пропадают из workInProgress-дерева. Но они сохраняются в свойстве deletions их родителя. Родителю проставляется флаг ChildDeletion и при коммите изменений все узлы из deletions будут удалены.

#fiber #подкапотом
👍5
Чем полезен тип unknown?

Статья (англ.): https://michaeluloth.com/programming-types-unknown-why-useful/

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


// не доверяем пришедшим данным
const getUserInput = (): unknown => {/*...*/}

const safe = () => {
const data = getUserInput()

if (typeof data === 'string') { // явно валидируем
data.toUpperCase() // используем метод строки
} else {
// обрабатываем некорректный тип
}
}


Такой подход позволяет избежать ряда ошибок, например, мы не сможем использовать метод .toUpperCase, пока не докажем компилятору, что это строка.

Пример не самый идеальный и в целом можно решать проблему неожиданных типов другими путями (например, использовать try-catch), но эта статья делает две хорошие вещи:

- раскрывает сущность типа unknown
- напоминает, что нельзя доверять чужим данным

#ссылки #typescript
👍6