Работа Fiber-дерева наглядно
Ссылка: https://jser.pro/ddir/rie?reactVersion=18.3.1&snippetKey=hq8jm2ylzb9u8eh468
Очень крутая пошаговая демонстрация работы Fiber-механизма в React: при первом рендере и при обновлении компонента.
#ссылки #fiber #подкапотом
Ссылка: https://jser.pro/ddir/rie?reactVersion=18.3.1&snippetKey=hq8jm2ylzb9u8eh468
Очень крутая пошаговая демонстрация работы Fiber-механизма в React: при первом рендере и при обновлении компонента.
#ссылки #fiber #подкапотом
jser.pro
React Internals Explorer | Deeper Dive Into React
React Internals Explorer to easily inspect React internals, created by JSer.
👍7❤1
Как происходит ререндер в 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
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
jser.dev
How does React re-render internally?
If updates are triggered after mount, React re-renders with minimum DOM updates after diffing 2 versions of Fiber Tree.
👍4❤1🔥1
reconcileChildren
Итак, произошло какое-то изменение, и мы в процессе создания нового FiberTree: обходим все узлы по порядку и создаем их свежие версии.
Новое дерево создается шаг за шагом, из текущего узла создаются дочерние, и возвращается первый из них - этим занимается функция
Она принимает:
⁃ старую (current) и новую (workInProgress) версию текущего fiber-узла
⁃
⁃ приоритеты рендеринга (
Функция определяет, что именно у нас отрендерилось в newChild, и на основе этого выбирает нужную ветку для создания нового FiberNode:
⁃
⁃
⁃
reconcileSingleElement
Создаем fiber, если у нас только один дочерний ReactElement.
⁃ проверяем, совпадает ли тип элемента со старой версией,
если да переиспользуем старый fiber (
⁃ удаляем все остальные дочерние элементы старой версии (если они были, т. е. если раньше было несколько элементов, а сейчас остался один) -
reconcileChildrenArray
Тут много оптимизаций, связанных с порядком элементов (key) и переиспользованием старых узлов.
Кроме того, каждый элемент массива может иметь разные типы, поэтому внутри функции еще несколько разветвлений, которые примерно похожи на обработку индивидуальных узлов.
Тут также проставляются флаги:
⁃ для удаленных старых элементов (
⁃ для вставки новых элементов (
Удаленные узлы
Важно: удаленные «старые» версии узлов пропадают из workInProgress-дерева. Но они сохраняются в свойстве
#fiber #подкапотом
Итак, произошло какое-то изменение, и мы в процессе создания нового 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 до тех пор, пока они не пройдут явную валидацию.
Такой подход позволяет избежать ряда ошибок, например, мы не сможем использовать метод .toUpperCase, пока не докажем компилятору, что это строка.
Пример не самый идеальный и в целом можно решать проблему неожиданных типов другими путями (например, использовать try-catch), но эта статья делает две хорошие вещи:
- раскрывает сущность типа unknown
- напоминает, что нельзя доверять чужим данным
#ссылки #typescript
Статья (англ.): 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
Michael Uloth
Why Unknown Types Are Useful
Sometimes you want the type checker to help you avoid making assumptions.
👍6