Разберись, как Fiber согласует дерево, группирует обновления и делает работу рендеринга прерываемой.
Открыть этот урок в KodokonНачиная с React 16 внутренний движок называется Fiber: каждый элемент твоего дерева представлен узлом fiber, единицей работы. На самом деле React держит два дерева (приём двойной буферизации): дерево current, которое отражает то, что на экране, и дерево workInProgress, которое строится во время рендеринга. Работа идёт в двух разных фазах: фаза рендеринга (вызов твоих компонентов, вычисление различий), прерываемая и воспроизводимая заново, затем фаза коммита (применение мутаций к DOM, запуск эффектов), которая всегда синхронна и непрерываема. Согласование опирается на две эвристики, сводящие сравнение к линейной стоимости: если тип элемента меняется в данной позиции, React уничтожает всё поддерево и строит его заново, вместе с состоянием; для списков проп key позволяет React сопоставить элементы между двумя рендерами.
function List({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
<input defaultValue={item.label} />
</li>
))}
</ul>
);
}
// With key={index}, deleting the first item
// would shift the content of the remaining inputs.Второй фундаментальный механизм - батчинг. Когда несколько обновлений состояния запускаются в одном и том же контексте, React объединяет их в один рендер. До React 18 такая группировка работала только внутри обработчиков событий React; начиная с createRoot она автоматическая везде: таймеры, промисы, нативные слушатели. Именно поэтому чтение состояния сразу после setState возвращает старое значение: обновление запланировано, а не применено.
function handleTick() {
setCount((c) => c + 1);
setFlag((f) => !f);
// React 18: a single render, even here.
}
setTimeout(handleTick, 100);
// Before React 18, this timer would trigger
// two successive renders.Конкурентный рендеринг идёт ещё дальше: фаза рендеринга может быть прервана, возобновлена или даже отброшена и запущена заново, если приходит более срочное обновление. React ранжирует обновления по приоритету (lanes): нажатие клавиши срочно, а фильтрация списка, помеченная через startTransition, нет. Прямое следствие: функции твоих компонентов должны быть чистыми, потому что React может вызвать их несколько раз, так и не закоммитив результат. Ещё один крайний случай: внешнее хранилище (Redux, Zustand, самописный синглтон), прочитанное во время прерываемого рендера, может измениться посреди рендера, порождая несогласованный интерфейс - это называется tearing. API useSyncExternalStore решает эту проблему, форсируя синхронный рендер при изменении хранилища.
import { useSyncExternalStore } from 'react';
function useWindowWidth() {
return useSyncExternalStore(
(notify) => {
window.addEventListener('resize', notify);
return () =>
window.removeEventListener('resize', notify);
},
() => window.innerWidth
);
}<div> становится <span> в той же позиции?createRoot сколько рендеров вызовут два вызова setState внутри setTimeout?