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