React 19 будет компилируемым
Статья (англ.): https://reacttraining.com/blog/react-19-will-be-compiled
В феврале разработчики React рассказали, что работают над компилятором (React Compiler), который добавит в React автоматическую мемоизацию и сделает жизнь разработчиков проще (пост тут).
А в статье автор разбирается, что же это значит и насколько это нововведение изменит разработку.
Зачем мы мемоизируем
Одним из важных отличий классовых компонентов от функциональных было то, что рендер был вынесен в отдельный метод класса. Там был только код рендера и больше ничего. Когда ваш компонент перерендеривался, он не дергал никакого лишнего кода, как это происходит сейчас с функциональными компонентами.
При каждом рендере у нас создаются новые функции, новые объекты, которые не равны тем, что были в предыдущем рендере, а значит, могут ненароком вызвать лишние рендеры дочерних компонентов. Чтобы избежать этого, мы используем всякие useCallback'и и useMemo - занимаемся ручной мемоизацией.
В статье приводится простой пример этого процесса.
Компилятор вроде как должен избавить нас от этой мороки, будем надеяться, что эта задумка будет удачной.
Чуть более компилируемый
Затем автор рассуждает немного о "компилируемости" React. Он приходит к заключению, что нет двух отдельных положений - либо компилируется, либо не компилируется. Компилируемость - это шкала, на которой одни инструменты (Svelte) более компилируемы, а другие менее.
Идея в том, что появление нового компилятора не должно нас пугать.
#ссылки
Статья (англ.): https://reacttraining.com/blog/react-19-will-be-compiled
В феврале разработчики React рассказали, что работают над компилятором (React Compiler), который добавит в React автоматическую мемоизацию и сделает жизнь разработчиков проще (пост тут).
А в статье автор разбирается, что же это значит и насколько это нововведение изменит разработку.
Зачем мы мемоизируем
Одним из важных отличий классовых компонентов от функциональных было то, что рендер был вынесен в отдельный метод класса. Там был только код рендера и больше ничего. Когда ваш компонент перерендеривался, он не дергал никакого лишнего кода, как это происходит сейчас с функциональными компонентами.
При каждом рендере у нас создаются новые функции, новые объекты, которые не равны тем, что были в предыдущем рендере, а значит, могут ненароком вызвать лишние рендеры дочерних компонентов. Чтобы избежать этого, мы используем всякие useCallback'и и useMemo - занимаемся ручной мемоизацией.
В статье приводится простой пример этого процесса.
Компилятор вроде как должен избавить нас от этой мороки, будем надеяться, что эта задумка будет удачной.
Чуть более компилируемый
Затем автор рассуждает немного о "компилируемости" React. Он приходит к заключению, что нет двух отдельных положений - либо компилируется, либо не компилируется. Компилируемость - это шкала, на которой одни инструменты (Svelte) более компилируемы, а другие менее.
Идея в том, что появление нового компилятора не должно нас пугать.
#ссылки
ReactTraining.com
React Will Be Compiled
React Corporate Workshops, Training, and Consulting
👍4
Использование forwardRef с generic-компонентами
Статья (англ.): https://www.totaltypescript.com/forwardref-with-generic-components
Если вы часто используете generic-компоненты (которые могут работать с разными типами данных), то возможно уже сталкивались с проблемой с пробрасыванием рефа.
Если обернуть такой компонент в функцию React.forwardRef, то внутри него перестают выводиться типы.
👉 Компонент:
👉 Использование:
Мы передаем массив чисел в проп data и ожидаем, что для параметра
👉 Решение:
Автор статьи предлагает сделать обертку над React.forwardRef, чтобы пофиксить это.
👉 Дополнительно:
- Перенаправление рефов
#ссылки #рефы
Статья (англ.): https://www.totaltypescript.com/forwardref-with-generic-components
Если вы часто используете generic-компоненты (которые могут работать с разными типами данных), то возможно уже сталкивались с проблемой с пробрасыванием рефа.
Если обернуть такой компонент в функцию React.forwardRef, то внутри него перестают выводиться типы.
👉 Компонент:
const Table = <T>(
props: {
data: T[];
renderRow: (row: T) => React.ReactNode;
},
ref: React.ForwardedRef<HTMLTableElement>
) => {
return (
<table ref={ref}>
<tbody>
{props.data.map((item, index) => (
<props.renderRow key={index} {...item} />
))}
</tbody>
</table>
);
};
const ForwardReffedTable = React.forwardRef(Table);
👉 Использование:
<ForwardReffedTable
data={[1, 2]}
renderRow={(row) => {
return <tr />;
}}
/>
Мы передаем массив чисел в проп data и ожидаем, что для параметра
row
выведется правильный тип number, но этого не происходит.👉 Решение:
Автор статьи предлагает сделать обертку над React.forwardRef, чтобы пофиксить это.
function fixedForwardRef<T, P = {}>(
render: (props: P, ref: React.Ref<T>) => React.ReactNode
): (props: P & React.RefAttributes<T>) => React.ReactNode {
return React.forwardRef(render) as any;
}
👉 Дополнительно:
- Перенаправление рефов
#ссылки #рефы
Total TypeScript
How To Use forwardRef With Generic Components
Learn about the limitations of React's `forwardRef` TypeScript and discover a solution to enable inference on generic components.
👍2
useActionState
В репозиторий реакт пару недель назад слили PR, в котором был немного изменен хук useFormState. Честно говоря, этот хук прошел полностью мимо меня, поэтому читаю как про совершенно новый хук)
Итак, у нас был какой-то useFormState, а стал useActionState, потому что все путались и думали, что можно работать только с формами. Оказывается, можно работать с любыми асинхронными экшенами.
Основная идея хука - отслеживать состояние экшена. Он возвращает три параметра:
Action
Вызвать его можно прямо:
А можно и традиционно засунуть в форму:
isPending
Как только запустился
Таким образом мы можем отслеживать состояние экшена, работает он прямо сейчас или нет.
state
Тут вроде ничего сложного нет, раньше мы самостоятельно накручивали подобный функционал вокруг какого-то вызова функции, теперь его накрутили за нас.
Конкуррентный рендеринг
Из приятного - под капотом используются transitions, то есть конкуррентный рендеринг.
Код выше можно переписать примерно вот так:
Подводные камни
Есть и сложности, куда без них.
Так как неизвестно, используется ли isPending, он всегда устанавливается перед вызовом экшена, что приводит к дополнительному обновлению стейта (та же проблема есть и у хука useTransition).
#ссылки #хуки
В репозиторий реакт пару недель назад слили PR, в котором был немного изменен хук useFormState. Честно говоря, этот хук прошел полностью мимо меня, поэтому читаю как про совершенно новый хук)
Итак, у нас был какой-то useFormState, а стал useActionState, потому что все путались и думали, что можно работать только с формами. Оказывается, можно работать с любыми асинхронными экшенами.
Основная идея хука - отслеживать состояние экшена. Он возвращает три параметра:
const [state, action, isPending] = useActionState(someAsyncAction)
Action
action
- это запуск экшена. То есть вместо того, чтобы напрямую вызывать someAsyncAction
, мы вызываем предоставленный им action
, так как в нем есть полезные оберточки.Вызвать его можно прямо:
async function handleAction() {
await action(someData)
}
А можно и традиционно засунуть в форму:
<form action={action}>
isPending
Как только запустился
action
, параметр isPending
становится равен true. Как только action
закончит свое выполнение, isPending
станет false.Таким образом мы можем отслеживать состояние экшена, работает он прямо сейчас или нет.
state
state
- это результат работы экшена, то, что он вернул. Если вызовов было несколько, то результат работы последнего вызова.Тут вроде ничего сложного нет, раньше мы самостоятельно накручивали подобный функционал вокруг какого-то вызова функции, теперь его накрутили за нас.
Конкуррентный рендеринг
Из приятного - под капотом используются transitions, то есть конкуррентный рендеринг.
Код выше можно переписать примерно вот так:
const [state, setState] = useState(null)
const [isPending, startTransition] = useTransition()
function handleAction() {
startTransition(async () => {
const response = await someAsyncAction({ userId: 222 })
setState(response)
})
}
Подводные камни
Есть и сложности, куда без них.
Так как неизвестно, используется ли isPending, он всегда устанавливается перед вызовом экшена, что приводит к дополнительному обновлению стейта (та же проблема есть и у хука useTransition).
#ссылки #хуки
GitHub
Add `React.useActionState` by rickhanlonii · Pull Request #28491 · facebook/react
Overview
Depends on #28514
This PR adds a new React hook called useActionState to replace and improve the ReactDOM useFormState hook.
Motivation
This hook intends to fix some of the confusion and l...
Depends on #28514
This PR adds a new React hook called useActionState to replace and improve the ReactDOM useFormState hook.
Motivation
This hook intends to fix some of the confusion and l...
👍5
Как React работает под капотом. Обзор "внутренностей"
Статья (англ.): https://jser.dev/2023-07-11-overall-of-react-internals
Видео (англ.): https://youtu.be/fgxxoaeKQWo?si=2pSYXMZOoxROdh-q
Автор потратил много времени, разбираясь во внутренностях React, и наконец решил изложить все это в цикле статей. Это первая статья, в которой мы разбираемся, как работает React, на самом верхнем уровне.
Тут у нас есть 4 фазы.
1. Триггер
Мы сообщаем React, что нужно что-то отрендерить (начальный рендер) или перерендерить, потому что произошли изменения.
Грубо говоря, тут создается "таск" и отправляется в планировщик (Scheduler).
Здесь отрабатывают следующие методы:
- scheduleUpdateOnFiber()
- ensureRootIsScheduled()
- scheduleCallback()
2. Планирование
Планировщик - это очередь с приоритетами. Метод scheduleCallback добавляет новый таск в очередь, а затем он попадает в workLoop().
3. Рендер
На этом шаге вычисляется новое Fiber Tree (то, что раньше называлось Virtual DOM). Этот процесс теперь может прерываться (конкуррентный рендер).
Тут работает метод performConcurrentWorkOnRoot()
4. Коммит
И наконец вычисленные изменения вносятся в реальный DOM (comminMutationEffects()). Тут также выполняются все виды эффектов (flushPassiveEffects(), commitLayoutEffects()).
Статья хорошая, в начале автор разбирает, как именно происходит процесс дебаггинга React-приложения, и где какие методы выполняются.
#ссылки #подкапотом #fiber
Статья (англ.): https://jser.dev/2023-07-11-overall-of-react-internals
Видео (англ.): https://youtu.be/fgxxoaeKQWo?si=2pSYXMZOoxROdh-q
Автор потратил много времени, разбираясь во внутренностях React, и наконец решил изложить все это в цикле статей. Это первая статья, в которой мы разбираемся, как работает React, на самом верхнем уровне.
Тут у нас есть 4 фазы.
1. Триггер
Мы сообщаем React, что нужно что-то отрендерить (начальный рендер) или перерендерить, потому что произошли изменения.
Грубо говоря, тут создается "таск" и отправляется в планировщик (Scheduler).
Здесь отрабатывают следующие методы:
- scheduleUpdateOnFiber()
- ensureRootIsScheduled()
- scheduleCallback()
2. Планирование
Планировщик - это очередь с приоритетами. Метод scheduleCallback добавляет новый таск в очередь, а затем он попадает в workLoop().
3. Рендер
На этом шаге вычисляется новое Fiber Tree (то, что раньше называлось Virtual DOM). Этот процесс теперь может прерываться (конкуррентный рендер).
Тут работает метод performConcurrentWorkOnRoot()
4. Коммит
И наконец вычисленные изменения вносятся в реальный DOM (comminMutationEffects()). Тут также выполняются все виды эффектов (flushPassiveEffects(), commitLayoutEffects()).
Статья хорошая, в начале автор разбирает, как именно происходит процесс дебаггинга React-приложения, и где какие методы выполняются.
#ссылки #подкапотом #fiber
👍11
Немного о Fiber
Суть работы React в том, что конечный интерфейс зависит от состояния, и при изменении состояния обновляется и интерфейс. Но он обновляется не полностью с нуля, а с огромным количеством оптимизаций. React вычисляет самый оптимальный способ обновить дерево компонентов, и для этого у него есть механизм Fiber.
Раньше был другой механизм, но его полностью переписали. Новый механизм Fiber обрабатывает изменения поэтапно, и между этапами его можно прервать. Это необходимо для реализации "конкурентных" фич React.
При каждом перерендере создается новое FiberTree, начиная от самого корня приложения/измененного элемента. Можно грубо сказать, что каждый Fiber соответствует React-компоненту. А в корне дерева находится FiberRoot.
Реконсилятор сравнивает новое дерево с текущим и вычисляет оптимальный набор обновлений. После внесения изменений (фаза commit), новое дерево само станет текущим.
В ближайших постах попробуем хотя бы в общих чертах разобраться, как именно это происходит:
- как движок узнает об изменениях
- как происходит обход дерева и реконсиляция
- как Fiber позволяет реализовать конкурентный рендеринг
и всякое такое
А для начала посмотрим, что из себя представляет отдельный файбер.
🪢 Fiber
Файбер - это минимальная единица работы при реконсиляции. По сути, обработка одного файбера - это обработка изменений одного компонента. То есть файбер описывает состояние компонента.
Файбер - это простой объект с кучей полей:
-
-
-
-
-
-
-
-
🪢 FiberRoot
Кроме обычных файберов есть еще FiberRoot, который соответствует корню приложения. У него еще больше полей, среди которых самое важное для нас:
-
#подкапотом #fiber
Суть работы React в том, что конечный интерфейс зависит от состояния, и при изменении состояния обновляется и интерфейс. Но он обновляется не полностью с нуля, а с огромным количеством оптимизаций. React вычисляет самый оптимальный способ обновить дерево компонентов, и для этого у него есть механизм Fiber.
Раньше был другой механизм, но его полностью переписали. Новый механизм Fiber обрабатывает изменения поэтапно, и между этапами его можно прервать. Это необходимо для реализации "конкурентных" фич React.
При каждом перерендере создается новое FiberTree, начиная от самого корня приложения/измененного элемента. Можно грубо сказать, что каждый Fiber соответствует React-компоненту. А в корне дерева находится FiberRoot.
Реконсилятор сравнивает новое дерево с текущим и вычисляет оптимальный набор обновлений. После внесения изменений (фаза commit), новое дерево само станет текущим.
В ближайших постах попробуем хотя бы в общих чертах разобраться, как именно это происходит:
- как движок узнает об изменениях
- как происходит обход дерева и реконсиляция
- как Fiber позволяет реализовать конкурентный рендеринг
и всякое такое
А для начала посмотрим, что из себя представляет отдельный файбер.
🪢 Fiber
Файбер - это минимальная единица работы при реконсиляции. По сути, обработка одного файбера - это обработка изменений одного компонента. То есть файбер описывает состояние компонента.
Файбер - это простой объект с кучей полей:
-
tag
- тип сущности, с которой связан файбер. Таких типов 28 (функциональный компонент, классовый компонент, провайдер-контекста, suspense и пр.). Отдельно отметим HostRoot (корень host-дерева) и IndeterminateComponent. При инициализации файбера в этом поле всегда будет IndeterminateCoomponent, пока не будет определен настоящий тип.-
type
- тип самого файбера. Для функционального компонента это ссылка на конкретную функцию, для классового - на класс, для хука - указатель на хук, для HostRoot - конкретный тег и т.д.-
stateNode
- ссылка на сопоставленную DOM-ноду.-
child
, sibling
, return
- поля, обеспечивающие структуру FiberTree (наподобие связного списка). Файбер указывает на своего первого ребенка (child), на свой соседний файбер (sibling), а также на родителя, к которому нужно вернуться после выполнения компонента (return).-
memoizedProps
, pendingProps
- старые и новые пропы. При обработке они сравниваются и если изменений нет, можно сделать вывод, что компонент не изменился и не нужно тратить силы на его перерендер. -
flags
, subtreeFlags
- это битовая маска, в которой хранится информация о состоянии самого файбера и его поддерева. Информация записывается сюда после выполнения работы. Например, мы сравнили старые и новые пропы и увидели, что требуется обновить данный узел. Мы запишем сюда флаг UPDATE - узел требует перерендера. При дальнейшей обработке движок увидит этот флаг и сделает необходимые действия для перерендера.-
lanes
, childLanes
- тоже битовая маска, определяющая файбер в какой-либо лэйн (полосу). Это нужно для приоритизации задач - некоторые лэйны приоритетнее, чем другие.-
alternate
- это копия файбера, в которой и происходят изменения во время реконсиляции. В сам файбер изменения вносятся только после коммита.🪢 FiberRoot
Кроме обычных файберов есть еще FiberRoot, который соответствует корню приложения. У него еще больше полей, среди которых самое важное для нас:
-
current
- ссылка на файбер, который сейчас находится в работе. В процессе обхода дерева реконсилятором эта ссылка будет изменяться. Таким образом, корневой файбер хранит состояние всего обхода дерева.#подкапотом #fiber
👍8🔥1👏1
Первый рендеринг в React под капотом (начало)
Статья (англ.): https://jser.dev/2023-07-14-initial-mount/
Статья очень длинная, подробная, с большим количеством исходного кода React.
Создание root
Стандартное react-приложение начинается со следующего кода:
Прежде всего, вспоминаем, что react-dom - это рендер. React предоставляет весь функционал для работы с состоянием приложения, а рендеры делают обертки приложения для отрисовки в конкретной среде - в данном случае в браузере.
root - это объект ReactDOMRoot, обертка. Но под капотом у него - FiberTree. Оно представлено корневой нодой FiberRoot, а также Fiber-нодой корневого DOM-элемента (tag: HostRoot).
Триггер обновления
Когда мы вызываем метод
Внутри этой функции происходит определение приоритета обновления (запрос лэйна). Если не ошибаюсь, то для первого рендера всегда будет DefaultLane, определяющий высокий приоритет и блокирующий синхронный рендеринг.
Здесь создается объект обновления
Обновление ставится в очередь планировщика и планировщик запускается (`scheduleUpdateOnFiber`).
Таким образом, планировщик начал свой WorkLoop, и у него в очереди находится один апдейт - вставка компонента приложения в хостовый элемент.
На этом фаза триггера заканчивается.
#подкапотом #fiber #ссылки
Статья (англ.): https://jser.dev/2023-07-14-initial-mount/
Статья очень длинная, подробная, с большим количеством исходного кода React.
Создание root
Стандартное react-приложение начинается со следующего кода:
import { createRoot } from 'react-dom/client'
const $appContainer = document.getElementById('root')
const root = createRoot($appContainer)
root.render(children)
Прежде всего, вспоминаем, что react-dom - это рендер. React предоставляет весь функционал для работы с состоянием приложения, а рендеры делают обертки приложения для отрисовки в конкретной среде - в данном случае в браузере.
root - это объект ReactDOMRoot, обертка. Но под капотом у него - FiberTree. Оно представлено корневой нодой FiberRoot, а также Fiber-нодой корневого DOM-элемента (tag: HostRoot).
const hostRootFiberNode = {
// ...
tag: HostRoot,
}
const fiberRootNode = {
// ...
tag: ConcurrentRoot,
containerInfo: $appContainer,
current: hostRootFiberNode,
}
Триггер обновления
Когда мы вызываем метод
root.render(children)
, он под капотом вызывает функцию updateContainer(children, fiberRootNode)
, передавая ей корень FiberTree.Внутри этой функции происходит определение приоритета обновления (запрос лэйна). Если не ошибаюсь, то для первого рендера всегда будет DefaultLane, определяющий высокий приоритет и блокирующий синхронный рендеринг.
Здесь создается объект обновления
update
, который содержит информацию о приоритете и дочерних элементах, которые нужно вставить в хост-элемент. Обновление ставится в очередь планировщика и планировщик запускается (`scheduleUpdateOnFiber`).
Таким образом, планировщик начал свой WorkLoop, и у него в очереди находится один апдейт - вставка компонента приложения в хостовый элемент.
На этом фаза триггера заканчивается.
#подкапотом #fiber #ссылки
jser.dev
How does React do the initial mount internally?
Initial mount is the first render for a React app, it creates the internal Fiber Tree and also the DOM tree.
👍4❤1
Первый рендеринг в React под капотом (продолжение)
Статья (англ.): https://jser.dev/2023-07-14-initial-mount/
Цикл планировщика
Теперь перемещаемся в цикл обновлений и фазу рендера.
Тут сразу важно отметить, что у планировщика есть некоторое внутреннее состояние, в частности переменные
Когда запускается процесс обновления состояния, планировщик получает FiberRootNode и копирует его в
Начинается все в функции
Тут у нас начинается цикл while, в котором за раз обрабатывается один юнит работы - то есть один файбер (метод
У нас пока один файбер: hostRootFiberNode.
beginWork
Основная работа происходит в функции
Здесь по типу файбера определяется, как он будет рендериться. Наш файбер имеет тег HostRoot, поэтому его обработкой будет заниматься функция
Только тут мы наконец доходим до очереди обновлений и достаем ранее созданный апдейт (в фазе триггера). Вытаскиваем из него children и запускаем реконсиляцию потомков:
Здесь возможна вилка. Если текущего элемента еще нет (первая отрисовка элемента), то дети создаются с нуля (`mountChildFibers`). Если есть, значит это ререндер и дети обновляются (`reconcileChildFibers`). У корневого файбера всегда есть current-состояние (но current.child = null), поэтому идем по второй ветке.
Дальше идет много функций для создания/реконсиляции самых разных типов узлов, с большим количеством условий, думаю, в их логику можно не погружаться. Если интересно, в статье очень много исходного кода, можно проследить вплоть до рендера последней ссылочки.
Вкратце, для каждого компонента создается файбер (с учетом текущего состояния) и помещается в дерево.
Когда все поддерево файбера обработано, на нем вызывается метод
В итоге мы имеем полностью построенное обновленное FiberTree в переменной workInProgress - finishedWork. Все флаги, необходимые для превращения этого дерева в реальное представление расставлены (например, флаг, что элемент должен быть вставлен в DOM). Также подготовлены все необходимые DOM-узлы.
Фаза рендера закончена, переходим к коммиту изменений.
Коммит изменений
Тут происходят последние приготовления виртуального дерева, включая выполнение необходимых эффектов, привязку листнеров и т.п. И наконец привязка к реальному DOM.
#подкапотом #fiber #ссылки
Статья (англ.): https://jser.dev/2023-07-14-initial-mount/
Цикл планировщика
Теперь перемещаемся в цикл обновлений и фазу рендера.
Тут сразу важно отметить, что у планировщика есть некоторое внутреннее состояние, в частности переменные
workInProgressRoot
и workInProgress
. Когда запускается процесс обновления состояния, планировщик получает FiberRootNode и копирует его в
workInProgressRoot
. Таким образом создается рабочая копия, в которую и вносятся все изменения, чтобы не затронуть актуальное дерево.Начинается все в функции
performConcurrentWorkOnRoot
, которая хоть и имеет в названии слово concurrent, на самом деле не всегда запускает конкурентный рендеринг. Это зависит от приоритета (от параметра lane). Некоторые лэйны блокирующие, например, DefaultLane. Первый рендер всегда синхронный, поэтому запускается функция renderRootSync
.Тут у нас начинается цикл while, в котором за раз обрабатывается один юнит работы - то есть один файбер (метод
performUnitofWork`). Текущий файбер хранится в переменной `workInProgress
. После того как обработан текущий, туда кладется ссылка на следующий файбер в дереве, и цикл продолжается.У нас пока один файбер: hostRootFiberNode.
beginWork
Основная работа происходит в функции
beginWork(current, unitOfWork, renderLanes)
, где current
- это текущее состояние (из текущего файбер-дерева), а unitOfWork
обновленное.Здесь по типу файбера определяется, как он будет рендериться. Наш файбер имеет тег HostRoot, поэтому его обработкой будет заниматься функция
updateHostRoot(current, workInProgress, renderLanes)
. Только тут мы наконец доходим до очереди обновлений и достаем ранее созданный апдейт (в фазе триггера). Вытаскиваем из него children и запускаем реконсиляцию потомков:
reconcileChildren(current, workInProgress, children, renderLanes)
.Здесь возможна вилка. Если текущего элемента еще нет (первая отрисовка элемента), то дети создаются с нуля (`mountChildFibers`). Если есть, значит это ререндер и дети обновляются (`reconcileChildFibers`). У корневого файбера всегда есть current-состояние (но current.child = null), поэтому идем по второй ветке.
Дальше идет много функций для создания/реконсиляции самых разных типов узлов, с большим количеством условий, думаю, в их логику можно не погружаться. Если интересно, в статье очень много исходного кода, можно проследить вплоть до рендера последней ссылочки.
Вкратце, для каждого компонента создается файбер (с учетом текущего состояния) и помещается в дерево.
Когда все поддерево файбера обработано, на нем вызывается метод
completeWork
, где создается DOM-нода, привязанная к этому файберу. (помним, что реализацию методов представления предоставляет рендер)В итоге мы имеем полностью построенное обновленное FiberTree в переменной workInProgress - finishedWork. Все флаги, необходимые для превращения этого дерева в реальное представление расставлены (например, флаг, что элемент должен быть вставлен в DOM). Также подготовлены все необходимые DOM-узлы.
Фаза рендера закончена, переходим к коммиту изменений.
Коммит изменений
Тут происходят последние приготовления виртуального дерева, включая выполнение необходимых эффектов, привязку листнеров и т.п. И наконец привязка к реальному DOM.
#подкапотом #fiber #ссылки
jser.dev
How does React do the initial mount internally?
Initial mount is the first render for a React app, it creates the internal Fiber Tree and also the DOM tree.
👍6
Пожалуй, стоит все-таки чуть погрузиться в построение дерева компонентов при первом рендеринге, чтобы дальше было легче понимать, что происходит.
Рабочий цикл
Итак, React зашел в рабочий цикл и нашел наше запланированное обновление для HostRoot:
Здесь мы достаем children (App) и запускаем reconcileChildren на хост-элементе.
Mount или Reconcile
Тут выполняется собственно реконсиляция. Если элемент монтируется в первый раз, вызывается mountChildFibers.
Но у HostRoot всегда есть current-состояние (хоть оно и равно null), поэтому мы пойдет по другой ветке - reconcileChildFibers. На самом функция там одна и та же (createChildReconciler), но разный входной параметр.
Результатом реконсиляции является файбер-нода для дочернего компонента App, которая будет возвращена из
Реконсиляция и установка флагов
Для разных типов узлов реконсиляция выполняется по-разному. В нашем случае это ReactElement (App):
Из reconcileSingleElement мы получаем один файбер с актуальным состоянием дерева. Если текущего элемента еще нет, как у нас, файбер просто создается с нуля (createFiberFromElement). Изначально он имеет неопределенный тип (IndeterminateComponent).
Затем в placeSingleChild файбер маркируется флагами для вставки в DOM. На флаги влияет обновляется ли файбер или создается с нуля. Для нашего файбера ставится флаг Placement (вставить в DOM).
Обход дерева, обработка компонентов
Итак, мы провели реконсиляцию для HostRoot и получили файбер для компонента App.
Цикл renderRootSync продолжается, теперь на очереди этот свежесозданный файбер App. Его тег еще не определен, поэтому работаетmountIndeterminateComponent.
Тут компонент рендерится (renderWithHooks), а также определяется его тег (FunctionComponent). И вызывается уже знакомый нам метод reconcileChildren - уже не для HostRoot, а для App.
Но в этом случае предыдущего состояния еще нет, поэтому отработает mountChildFibers:
Для этого файбера не будет установлен флаг Placement, так как он маунтится с нуля.
Дальше цикл повторяется уже для нового файбера (потомка App). Для разных элементов будет отличаться путь обработки в beginWork:
В итоге все равно запустится reconcileChildren для текущего узла и будет создан новый дочерний файбер.
Завершение обработки поддерева
После того, как будет достигнут последний элемент в дереве (лист), React начинает завершать файбер (completeUnitOfWork):
На этом этапе создается DOM-узел, связанный с файбером, и записывается в свойство stateNode.
При этом сначала завершается листовой файбер, React поднимается выше и завершает все родительский файбер. Если у него есть сиблинги, то React переходит к обработке соседнего поддерева. Когда завершится корневой файбер, обработка завершена - переходим к фазе коммита изменений.
Где именно происходит вот это завершение, пока не очень понятно - где-то внутри
#ссылки #fiber #подкапотом
Рабочий цикл
Итак, React зашел в рабочий цикл и нашел наше запланированное обновление для HostRoot:
renderRootSync
workLoopSync
while(workInProgress) {
performUnitOfWork(workInProgress)
}
Здесь мы достаем children (App) и запускаем reconcileChildren на хост-элементе.
performUnitOfWork()
beginWork()
updateHostRoot()
reconcileChildren()
Mount или Reconcile
Тут выполняется собственно реконсиляция. Если элемент монтируется в первый раз, вызывается mountChildFibers.
Но у HostRoot всегда есть current-состояние (хоть оно и равно null), поэтому мы пойдет по другой ветке - reconcileChildFibers. На самом функция там одна и та же (createChildReconciler), но разный входной параметр.
Результатом реконсиляции является файбер-нода для дочернего компонента App, которая будет возвращена из
beginWork
и станет следующей в очереди на обработку в цикле workLoopSync.Реконсиляция и установка флагов
reconcileChildren
reconcileChildFibers
createChildReconciler(true)
reconcileChildFibersImpl
Для разных типов узлов реконсиляция выполняется по-разному. В нашем случае это ReactElement (App):
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement()
)
Из reconcileSingleElement мы получаем один файбер с актуальным состоянием дерева. Если текущего элемента еще нет, как у нас, файбер просто создается с нуля (createFiberFromElement). Изначально он имеет неопределенный тип (IndeterminateComponent).
Затем в placeSingleChild файбер маркируется флагами для вставки в DOM. На флаги влияет обновляется ли файбер или создается с нуля. Для нашего файбера ставится флаг Placement (вставить в DOM).
Обход дерева, обработка компонентов
Итак, мы провели реконсиляцию для HostRoot и получили файбер для компонента App.
Цикл renderRootSync продолжается, теперь на очереди этот свежесозданный файбер App. Его тег еще не определен, поэтому работаетmountIndeterminateComponent.
performUnitOfWork
beginWork
mountIndeterminateComponent
Тут компонент рендерится (renderWithHooks), а также определяется его тег (FunctionComponent). И вызывается уже знакомый нам метод reconcileChildren - уже не для HostRoot, а для App.
Но в этом случае предыдущего состояния еще нет, поэтому отработает mountChildFibers:
reconcileChildren
mountChildFibers
createChildReconciler(false)
Для этого файбера не будет установлен флаг Placement, так как он маунтится с нуля.
Дальше цикл повторяется уже для нового файбера (потомка App). Для разных элементов будет отличаться путь обработки в beginWork:
funсtion beginWork() {
case IndeterminateComponent:
mountIndeterminateComponent()
case HostComponent: // для HTML-тегов
updateHostComponent()
case HostText: // для текстовых узлов
updateHostText()
}
В итоге все равно запустится reconcileChildren для текущего узла и будет создан новый дочерний файбер.
Завершение обработки поддерева
После того, как будет достигнут последний элемент в дереве (лист), React начинает завершать файбер (completeUnitOfWork):
function performUnitOfWork(unitOfWork) {
next = beginWork(current, unitOfWork, subtreeRenderLanes)
if (next === null) {
...
} else {
workInProgress = next
}
}
На этом этапе создается DOM-узел, связанный с файбером, и записывается в свойство stateNode.
При этом сначала завершается листовой файбер, React поднимается выше и завершает все родительский файбер. Если у него есть сиблинги, то React переходит к обработке соседнего поддерева. Когда завершится корневой файбер, обработка завершена - переходим к фазе коммита изменений.
Где именно происходит вот это завершение, пока не очень понятно - где-то внутри
completeUnitOfWork
.#ссылки #fiber #подкапотом
👍4👏1
Схема обновления дерева компонентов:
https://youtu.be/kNypR7QKixE?si=AZcFKvHjLSO_L3y5
#react #fiber #подкапотом
https://youtu.be/kNypR7QKixE?si=AZcFKvHjLSO_L3y5
#react #fiber #подкапотом
YouTube
Fiber tree update
👍4👏1
React Fiber & Concurrency. Часть 1
Статья (рус.): https://habr.com/ru/articles/763534/
Очень сложно у меня идет погружение во внутренности React, несколько раз уже хотела что-то резюмировать, но как будто пока особо нечего.
Поэтому вот неплохая статья с обзором механизма React Fiber, но без глубокого погружения в структуры и цепочки методов. Будем создавать устойчивую базу для дальнейшего понимания.
React Fiber
Главная задача, которую решило внедрение Fiber в React - инкрементальный рендеринг, на основе которого затем реализуется возможность неблокирующего рендеринга.
Весь процесс обновления дерева компонентов глобально состоит из двух фаз:
⁃ render (reconciliation) - тут применяются обновления, сравниваются старое и новое состояние и определяется, какую работу нужно выполнить, чтобы все обновить
⁃ commit - здесь вызываются хуки (или методы жизненного цикла), а также обновляется визуал (DOM).
Fiber Tree
Структура и состояние приложения хранится в виде Fiber Tree - причем таких деревьев у нас будет целых два: currentTree (текущее состояние, которое видит пользователь) и workInProgressTree (новое состояние, которое нужно применить).
У каждого дерева есть корень - FiberRoot, который создается в функции createWorkInProgress. При инициализации приложения (ReactDOM.createRoot) создается как раз этот корень для currentTree.
Каждый узел такого дерева - Fiber - это по сути минимальный кусочек работы (обновление компонента). Fiber создается на основе React Element (createFiberFromElement).
Первая фаза
Во время первой фазы (рендер) сначала создается корень для workInProgressTree на основе корня currentTree (createWorkInProgress).
Далее мы последовательно обходим дерево workInProgressTree, начиная с корня. и для каждого узла выполняем некоторую работу (performUnitOfWork). Внутри собственно и происходит вся работа по обновлению и реконсилляции.
Сначала происходит собственно обновление текущего активного узла - функция beginWork. Здесь выполняется вызов компонента (функционального или классового, не важно) и создание fiber-узла для него. Если Fiber создается впервые (нет соответствующего узла в currentTree), то срабатывает функция createFiberFromElement, иначе узел обновляется на основе старой версии в функции createWorkInProgress.
Далее переходим к первому ребенку текущего узла и обрабатываем его. Цикл продолжается, пока не попадется узел без детей (конец ветки). Тогда сработает функция completeUnitOfWork - завершение обработки «ветки». В ней происходит «всплытие» до первого родительского элемента, которого есть необработанный сиблинг, то есть цикл продолжится на следующей ветке.
В процессе обновления заполняются поля flags и subtreeFlags - это пометки о том, какие действия нужно будет провести над этими узлами в фазе коммита изменений (например, есть флаги, указывающие, что нужно сделать снапшот - Snapshot или обновить узел - Update). Логично, что subtreeFlags заполняется во время всплытия (completeeUnitOfWork), когда поддерево узла уже обработано и все флаги для дочерних узлов проставлены.
Также в completeUnitOfWork модифицируется DOM-элемент, соответствующий узлу (он записывается в поле stateNode).
Вторая фаза
К этому моменту полностью сформировано обновленное дерево workInProgressTree, которое записывается в переменную finishedWork.
Здесь будет происходить обработка на основе ранее проставленных флагов. Действия выполняются в следующем порядке:
⁃ commitBeforeMutationEffects - действия перед обновлением DOM (снапшоты)
⁃ commitMutationEffects - изменения DOM (отрисовка происходит позже, когда у браузера будет возможность)
⁃ commitLayoutEffects - выполнение хуков useLayoutEffect
⁃ flushPassiveEffects - выполнение хуков useEffect
С помощью флагов React находит в дереве узлы, с которыми нужно выполнить то или иное действие (используется двоичная маска).
#ссылки #fiber #подкапотом
Статья (рус.): https://habr.com/ru/articles/763534/
Очень сложно у меня идет погружение во внутренности React, несколько раз уже хотела что-то резюмировать, но как будто пока особо нечего.
Поэтому вот неплохая статья с обзором механизма React Fiber, но без глубокого погружения в структуры и цепочки методов. Будем создавать устойчивую базу для дальнейшего понимания.
React Fiber
Главная задача, которую решило внедрение Fiber в React - инкрементальный рендеринг, на основе которого затем реализуется возможность неблокирующего рендеринга.
Весь процесс обновления дерева компонентов глобально состоит из двух фаз:
⁃ render (reconciliation) - тут применяются обновления, сравниваются старое и новое состояние и определяется, какую работу нужно выполнить, чтобы все обновить
⁃ commit - здесь вызываются хуки (или методы жизненного цикла), а также обновляется визуал (DOM).
Fiber Tree
Структура и состояние приложения хранится в виде Fiber Tree - причем таких деревьев у нас будет целых два: currentTree (текущее состояние, которое видит пользователь) и workInProgressTree (новое состояние, которое нужно применить).
У каждого дерева есть корень - FiberRoot, который создается в функции createWorkInProgress. При инициализации приложения (ReactDOM.createRoot) создается как раз этот корень для currentTree.
Каждый узел такого дерева - Fiber - это по сути минимальный кусочек работы (обновление компонента). Fiber создается на основе React Element (createFiberFromElement).
Первая фаза
Во время первой фазы (рендер) сначала создается корень для workInProgressTree на основе корня currentTree (createWorkInProgress).
Далее мы последовательно обходим дерево workInProgressTree, начиная с корня. и для каждого узла выполняем некоторую работу (performUnitOfWork). Внутри собственно и происходит вся работа по обновлению и реконсилляции.
Сначала происходит собственно обновление текущего активного узла - функция beginWork. Здесь выполняется вызов компонента (функционального или классового, не важно) и создание fiber-узла для него. Если Fiber создается впервые (нет соответствующего узла в currentTree), то срабатывает функция createFiberFromElement, иначе узел обновляется на основе старой версии в функции createWorkInProgress.
Далее переходим к первому ребенку текущего узла и обрабатываем его. Цикл продолжается, пока не попадется узел без детей (конец ветки). Тогда сработает функция completeUnitOfWork - завершение обработки «ветки». В ней происходит «всплытие» до первого родительского элемента, которого есть необработанный сиблинг, то есть цикл продолжится на следующей ветке.
В процессе обновления заполняются поля flags и subtreeFlags - это пометки о том, какие действия нужно будет провести над этими узлами в фазе коммита изменений (например, есть флаги, указывающие, что нужно сделать снапшот - Snapshot или обновить узел - Update). Логично, что subtreeFlags заполняется во время всплытия (completeeUnitOfWork), когда поддерево узла уже обработано и все флаги для дочерних узлов проставлены.
Также в completeUnitOfWork модифицируется DOM-элемент, соответствующий узлу (он записывается в поле stateNode).
Вторая фаза
К этому моменту полностью сформировано обновленное дерево workInProgressTree, которое записывается в переменную finishedWork.
Здесь будет происходить обработка на основе ранее проставленных флагов. Действия выполняются в следующем порядке:
⁃ commitBeforeMutationEffects - действия перед обновлением DOM (снапшоты)
⁃ commitMutationEffects - изменения DOM (отрисовка происходит позже, когда у браузера будет возможность)
⁃ commitLayoutEffects - выполнение хуков useLayoutEffect
⁃ flushPassiveEffects - выполнение хуков useEffect
С помощью флагов React находит в дереве узлы, с которыми нужно выполнить то или иное действие (используется двоичная маска).
#ссылки #fiber #подкапотом
Хабр
React Fiber & Concurrency Part 1 (2)
В сети много статей и докладов, которые описывают React Fiber, но, к сожалению, они сейчас уже не актуальны. Заметив это, я решил разобраться и актуализировать информацию. Моими основными помощниками...
👍3
Инициализация React-приложения и запуск первого рендера (верхнеуровневый обзор)
Еще одно небольшое верхнеуровневое резюме по самому первому этапу работы React-приложения (инициализация и запуск первого рендера).
Основано на:
👉 How createRoot works
👉 How root.render() works
👉 How root render schedule works
Начало
Начинается все с простого синхронного кода:
1 - Создание корня
На первом шаге мы создаем «корень React-приложения» - структуру ReactDOMRoot, которая содержит ссылку на FiberTree всего приложения.
FiberTree состоит из отдельных файберов FiberNode, а на самом верху у него корневой файбер FiberRootNode.
При создании у нас уже есть корневой файбер + еще один обычный файбер, соответствующий корневому DOM-элементу, в который и будет рендериться наше React-приложение - нужно же с чего-то начинать. Этот файбер хранится в поле
2 - Запуск рендера
На втором шаге мы впервые запускаем процесс обновления приложения. Сейчас вручную, а дальше это будет происходить автоматически при изменениях, например, стейта. За это отвечает функция
Обновление начинается с самого верха - с корня файбер-дерева и того самого предзаготовленного первого файбера из поля current.
React определяет полосу (приоритет) обновления для этого файбера (lane) - при первом рендере приоритет очень высокий.
Затем создается объект обновления (Update), где сохраняются все необходимые данные (ссылка на файбер, приоритет и собственно содержимое (children -
Объект обновления сохраняется внутри файбера в очереди обновлений (updateQueue).
Само обновление будет происходить позже и вообще в другом месте - внутри цикла React. Поэтому сейчас его нужно только оформить и запланировать - чтобы цикл потом мог его найти.
Этим занимается функция
Когда все запланировано, запускается синхронный WorkLoop (функция `flushSyncWorkOnAllRoots`), и это уже совсем другая история.
#подкапотом #fiber #ссылки
Еще одно небольшое верхнеуровневое резюме по самому первому этапу работы React-приложения (инициализация и запуск первого рендера).
Основано на:
👉 How createRoot works
👉 How root.render() works
👉 How root render schedule works
Начало
Начинается все с простого синхронного кода:
import { App } from "./app";
import { createRoot } from "react-dom/client";
const container = document.getElementById("root");
// 1
const root = createRoot(container);
// 2
root.render(<App />);
1 - Создание корня
На первом шаге мы создаем «корень React-приложения» - структуру ReactDOMRoot, которая содержит ссылку на FiberTree всего приложения.
FiberTree состоит из отдельных файберов FiberNode, а на самом верху у него корневой файбер FiberRootNode.
При создании у нас уже есть корневой файбер + еще один обычный файбер, соответствующий корневому DOM-элементу, в который и будет рендериться наше React-приложение - нужно же с чего-то начинать. Этот файбер хранится в поле
current
нашего файбер-дерева.2 - Запуск рендера
На втором шаге мы впервые запускаем процесс обновления приложения. Сейчас вручную, а дальше это будет происходить автоматически при изменениях, например, стейта. За это отвечает функция
updateContainer
.Обновление начинается с самого верха - с корня файбер-дерева и того самого предзаготовленного первого файбера из поля current.
React определяет полосу (приоритет) обновления для этого файбера (lane) - при первом рендере приоритет очень высокий.
Затем создается объект обновления (Update), где сохраняются все необходимые данные (ссылка на файбер, приоритет и собственно содержимое (children -
<App />
), которое нужно вставить в контейнер. Объект обновления сохраняется внутри файбера в очереди обновлений (updateQueue).
Само обновление будет происходить позже и вообще в другом месте - внутри цикла React. Поэтому сейчас его нужно только оформить и запланировать - чтобы цикл потом мог его найти.
Этим занимается функция
scheduleUpdateOnFiber
. Она планирует микротаск, который будет выполняться сразу после того, как закончится синхронный код. Как именно планируется микротаск, кажется, определяет сам рендерер для конкретного окружения (например, ReactDOM использует для планировки промисы). Тут важно, что обновление добавлено в Event Loop. Собственно момент планировки происходит в функции scheduleImmediateTask
.Когда все запланировано, запускается синхронный WorkLoop (функция `flushSyncWorkOnAllRoots`), и это уже совсем другая история.
#подкапотом #fiber #ссылки
incepter.github.io
How createRoot works | How React Works
When following a React tutorial or the docs, the first step that you should do
👍5
Как React обходит Fiber-дерево?
Статья (англ.): https://jser.dev/react/2022/01/16/fiber-traversal-in-react/
Очень маленькая статья, описывающая алгоритм обхода FiberTree в React.
Каждый узел дерева имеет поля child, sibling и return, то есть хранит ссылку на первого ребенка, следующего соседа и родителя.
При обходе мы сначала спускаемся до самого низа ветки по цепочке child'ов, а когда дети заканчиваются, завершаем обработку текущего узла и поднимается на уровень выше (по ссылке в return). Обработку родительского узла также завершаем. Здесь мы проверяем, есть ли сиблинг, если есть, идем к нему, если нет, поднимаемся еще на уровень выше.
В конце концов мы дойдем до самого низа самой правой ветки и оттуда поднимемся к корню дерева, у которого ничего нет в свойстве return - обход окончен.
#ссылки #подкапотом #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, от которого потом будет расти все дерево приложения.
На данный момент наше дерево представлено корнем FiberRoot, а также одним обычным Fiber типа HostRoot - он соответствует HTML-контейнеру приложения - тегу #app.
2. Триггер и планирование обновления
Первое обновление запускаем вручную, дальше они будут вызваться автоматически при изменении состояния компонентов, но это нам сейчас неинтересно:
Тут создается объект обновления для Fiber:HostRoot, внутри которого сохраняется элемент, который нужно отрендерить (<App />). Обновление помещается в очередь обновлений для Fiber:HostRoot (updateQueue).
На данном этапе обновление не происходит - оно только планируется на будущее.
Весь код до этого момента - синхронный.
3. Рабочий цикл планировщика
Переходим к циклу. У нас есть обновление, поэтому нужно создать новое Fiber-дерево. Буквально новое и прямо от корня - это новое дерево называется workInProgress.
Создаем новый Fiber:HostRoot - и начинаем цикл прямо с него.
На каждой итерации мы берем текущий Fiber-узел и - внимание! - создаем его child-узел. То есть с каждым шагом наше workInProgress-дерево растет.
Это делает функция
При создании child-узла для нового дерева обязательно учитывается, если у него уже есть текущая версия в current-дереве. Если есть, то старый узел будет обновлен, если еще нет, то узел будет создан заново из React-элемента.
Последовательность обхода дерева описана в предыдущем посте - сначала мы спускаемся вниз по дочерним узлам (сначала создаем дочерний узел, потом сразу же к нему переходим). Это фаза собственно «рендера» или «реконсиляции» - расчет изменений.
Как именно создается дочерний узел, зависит от типа текущего узла. Для HostRoot данные для рендера (дочерние элементы) достаются из того самого объекта обновления, который был создан и запланирован в самом начале. Для других узлов в основном берутся из пропсов (props.children).
Когда достигнут низ ветки и детей больше нет, начинаем подниматься вверх и искать первый узел, у которого есть сиблинг. При этом мы последовательно «закрываем» все узлы, которые уже были обработаны (снизу вверх) - это делает функция
На этом «обратном» пути для каждого узла создается соответствующий DOM-элемент, который пока просто сохраняется в поле
Также во время спуска и подъема проставляются разнообразные флаги, которые затем будут использоваться для обновления DOM (например, что узел нужно удалить или наоборот вставить в документ).
4. Коммит изменений в DOM
Итак, мы обошли все дерево, и теперь у нас есть полностью построенное дерево workInProgress, где для каждого узла проставлены флаги, что с ним нужно делать (например, вставить в DOM или удалить).
Теперь мы реально будем менять DOM - сначала удалять, потом вставлять и менять порядок, потом запускать эффекты.
Дерево workInProgress теперь становится деревом current.
#ссылки #fiber #подкапотом
Еще одна попытка разобраться в подкапотье механизма 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 #подкапотом
jser.dev
How does React do the initial mount internally?
Initial mount is the first render for a React app, it creates the internal Fiber Tree and also the DOM tree.
👍4
beginWork (часть 1)
Итак, самое интересное происходит внутри workLoop в функции
Внутри функции происходит создание дочернего Fiber-узла, который станет следующим в цикле. Тут нам как минимум нужно понять, что находится внутри следующего узла (`props.children`).
Мы берем две версии узла - ту, которая должна быть, и ту, которая есть сейчас - и сравниваем их, определяя минимальный набор изменений для поддерева - это и есть реконсиляция.
Как именно происходит это сравнение, зависит от типа узла -
updateHostRoot
Самым первым в функцию
Нужно узнать, какие дочерние элементы должны быть в этом узле (nextChildren) - откуда взять эту информацию? Мы помним, что при вызове функции
А дальше собственно реконсиляция:
Основная идея - на основе узла workInProgress и его current-версии создать дочерний узел из nextChildren и вернуть его, чтобы продолжить цикл.
reconcileChildren
Если у активного узла нет current-версии (он создается впервые), то дочерний узел будет создан функцией
Если current-версия есть, то необходимо сравнить их поддеревья -
На самом деле это почти одно и то же, под капотом там работает одна и та же функция
Собственно реализация процесса реконсиляции находится в функции
У нас изначально это элемент
Так как предыдущей версии для этого узла нет, он будет создан прямо из элемента -
Важно: при создании нового Fiber-узла его tag=IndeterminateComponent.
#ссылки #fiber #подкапотом
Итак, самое интересное происходит внутри 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)
В следующей итерации мы работаем со свежесозданным узлом для элемента
Функция другая, но идея та же - нужно создать дочерний узел. Для этого компонент вызывается со всеми пропсами (`renderWithHooks`) - и мы получаем его поддерево, из которого создается новый узел (тоже изначально с типом IndeterminateComponent).
Тут же происходит замена тега для активного узла - IndeterminateComponent меняется на более подходящий FunctionComponent. (свежесозданный дочерний узел остается неопределенным).
Цикл продолжается дальше, пока не дойдет до конца ветки. Для разных элементов вызываются разные функции, но идея не меняется - создание дочернего узла, пока есть что создавать. И везде работает функция
completeWork
Когда у текущего Fiber-узла дочернего узла нет, необходимо завершить работу над ним - completeUnitOfWork.
Тут происходит подъем с низа ветки до первого узла с сиблингом (по которому можно перейти к следующей ветке). По пути все встреченные узлы «завершаются» и для них проставляются необходимые флаги.
Для каждого узла создается соответствующий DOM-элемент (сохраняется в свойстве fiber.stateNode). Если current-версии нет, то создается с нуля, а если есть, то обновляется (updateHostComponent).
Важно: в DOM-элемент, соответствующий Fiber-узлу, вставляется все его поддерево (DOM-элементы дочерних узлов).
#ссылки #fiber #подкапотом
В следующей итерации мы работаем со свежесозданным узлом для элемента
<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
После того как в фазе рендера (реконсиляции) определены все изменения, нужно внести их в реальный 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
jser.dev
How does React do the initial mount internally?
Initial mount is the first render for a React app, it creates the internal Fiber Tree and also the DOM tree.
👍4
Работа 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