React Junior
207 subscribers
37 photos
462 links
Изучение React с нуля
加入频道
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