Понимание React Fiber
Чтобы включить в него информацию о ключевых улучшениях в React Fiber, начиная с React v16, таких как параллелизм, автоматическое пакетирование и новые перехватчики, такие как useTransition, useSyncExternalStore и useInsertionEffect.
Вы когда-нибудь задумывались, что происходит, когда вы вызываете ReactDOM.render(<App />, document.getElementById('root'))?
Мы знаем, что ReactDOM создает дерево DOM под капотом и отображает приложение на экране. Но как React на самом деле создает дерево DOM? И как оно обновляет дерево при изменении состояния приложения?
В этом посте мы узнаем, что такое React Fiber и как React строил дерево DOM до React версии 15.0.0, о подводных камнях этой модели и о том, как новая модель, начиная с React версии 16.0.0 и заканчивая текущей версией, решает эти проблемы.
В этом посте будет рассмотрен широкий спектр концепций, которые являются чисто внутренними деталями реализации и не являются строго необходимыми для реальной разработки интерфейса с использованием React.
Что такое React Fiber?
React Fiber - это внутреннее изменение в движке, направленное на то, чтобы сделать React более быстрым и интеллектуальным. Программа Fiber reconciler, которая стала программой согласования по умолчанию для React 16 и более поздних версий, представляет собой полную переработку алгоритма согласования React для решения некоторых давних проблем в React.
Поскольку Fiber является асинхронным, React может:
- Приостанавливать, возобновлять и перезапускать рендеринг компонентов по мере поступления новых обновлений
- Повторно используйте ранее выполненную работу и даже отменяйте ее, если в ней нет необходимости
- Разделите работу на части и расставьте приоритеты в зависимости от важности задач
Это изменение позволяет React выйти за рамки синхронного средства согласования стека. Ранее, например, вы могли добавлять или удалять элементы, но это должно было работать до тех пор, пока стек не опустеет, и задачи нельзя было прервать.
Это изменение также позволяет React точно настраивать компоненты рендеринга, гарантируя, что наиболее важные обновления будут внесены как можно скорее.
Теперь, чтобы по-настоящему понять возможности Fiber, давайте поговорим о старом примирителе: stack reconciler.
React’s stack reconciler
Давайте начнем с нашего знакомого ReactDOM.render(<App />, document.getElementById('root')).
Модуль ReactDOM передает <App/ > в программу согласования, но здесь возникают два вопроса:
- К чему относится <App />?
- Что такое reconciler?
Давайте разберемся с этими двумя вопросами.
Что такое <App />?
<App /> - это элемент React, а “элементы описывают дерево”. Согласно блогу React, “Элемент - это простой объект, описывающий экземпляр компонента или узел DOM и его желаемые свойства”.
Другими словами, элементы не являются настоящими узлами DOM или экземплярами компонентов; они представляют собой способ описания для React того, что это за элементы, какими свойствами они обладают и кто является их дочерними элементами.
Именно в этом заключается реальная сила React: React сам по себе абстрагирует сложные элементы создания, рендеринга и управления жизненным циклом реального дерева DOM, эффективно облегчая жизнь разработчика.
Чтобы понять, что это на самом деле означает, давайте рассмотрим традиционный подход, использующий объектно-ориентированные концепции.
Объектно-ориентированное программирование в React
В типичном мире объектно-ориентированного программирования разработчики должны создавать экземпляры и управлять жизненным циклом каждого элемента DOM. Например, если вы хотите создать простую форму и кнопку отправки, управление состоянием по-прежнему требует от разработчика определенных усилий.
Давайте предположим, что у компонента Button есть переменная состояния isSubmitted. Жизненный цикл компонента Button выглядит примерно так, как показано на блок-схеме ниже, где каждое состояние должно управляться приложением:
Этот размер блок-схемы и количество строк кода растут экспоненциально по мере увеличения числа переменных состояния.
Итак, в React есть элементы для решения этой проблемы; в React есть два вида элементов: элемент DOM и элемент component.
Элемент DOM - это элемент, представляющий собой строку; например, <button class="OKButton"> OK </button>.
Элементом component является класс или функция, например, <Button className="OKButton"> OK </Button>, где <Button> - это либо класс, либо функциональный компонент. Это типичные компоненты React, которые мы обычно используем.
Важно понимать, что оба типа являются простыми объектами. Они являются простым описанием того, что должно отображаться на экране, и не инициируют визуализацию при их создании и инстанцировании.
What is React reconciliation?
Это упрощает для React их синтаксический анализ и обход для построения дерева DOM. Фактический рендеринг происходит позже, когда обход завершается.
Когда React сталкивается с классом или функциональным компонентом, он запрашивает у этого элемента, какой элемент он отображает на основе его реквизита.
Например, если компонент <App> отобразил следующее, то React запросит компоненты <Form> и <Button>, что они отображают, основываясь на соответствующих реквизитах:
<Form> <Button> Submit </Button> </Form>
Итак, если компонент Form является функциональным компонентом, который выглядит следующим образом, React вызовет render(), чтобы узнать, какие элементы он отображает, и увидит, что он отображает <div> с дочерним элементом
const Form = (props) => {
return(
<div className="form">
{props.form}
</div>
)
}
React будет повторять этот процесс до тех пор, пока не узнает базовые элементы тегов DOM для каждого компонента на странице.
Именно этот процесс рекурсивного обхода дерева для определения базовых элементов DOM-тегов дерева компонентов приложения React известен как согласование.
К концу согласования React знает результат построения дерева DOM, и средство визуализации, такое как react-dom или react-native, применяет минимальный набор изменений, необходимых для обновления узлов DOM. Это означает, что когда вы вызываете ReactDOM.render() или setState(), React выполняет согласование.
В случае setState он выполняет обход и определяет, что изменилось в дереве, сравнивая новое дерево с отображаемым деревом. Затем он применяет эти изменения к текущему дереву, тем самым обновляя состояние, соответствующее вызову setState().
Теперь, когда мы понимаем, что такое согласование, давайте рассмотрим подводные камни этой модели.
Что такое React stack reconciler?
Да, кстати, почему это называется “стековый” выверщик? Это название происходит от структуры данных “stack”, которая представляет собой механизм "последний вход - первый выход".
И какое отношение stack имеет к тому, что мы только что видели? Ну, как оказалось, поскольку мы эффективно выполняем рекурсию, все это имеет отношение к stack.
Что такое рекурсия в React?
Чтобы понять, почему это так, давайте рассмотрим простой пример и посмотрим, что происходит в стеке вызовов:
function fib(n) {
if (n < 2){
return n
}
return fib(n - 1) + fib (n - 2)
}
fib(10)
Как мы можем видеть, стек вызовов помещает каждый вызов функции fib() в стек до тех пор, пока не появится функция fib(1), которая является первым возвращаемым вызовом функции.
Затем он продолжает выполнять рекурсивные вызовы и появляется снова, когда доходит до инструкции return. Таким образом, он эффективно использует стек вызовов до тех пор, пока не вернется fib(3) и не станет последним элементом, появляющимся из стека.
Алгоритм согласования, который мы только что видели, является чисто рекурсивным алгоритмом. Обновление приводит к немедленному повторному отображению всего поддерева. Хотя это работает хорошо, у него есть некоторые ограничения.
Как отмечает Эндрю Кларк, в пользовательском интерфейсе необязательно, чтобы каждое обновление применялось немедленно; на самом деле, это может быть расточительным, приводящим к удалению фреймов и ухудшению взаимодействия с пользователем.
Кроме того, разные типы обновлений имеют разные приоритеты — обновление анимации должно выполняться быстрее, чем обновление из хранилища данных.
Проблемы с удаленными кадрами
Теперь, что мы имеем в виду, когда говорим о пропущенных кадрах, и почему это является проблемой при рекурсивном подходе? Чтобы понять это, давайте кратко рассмотрим, что такое частота кадров и почему она важна с точки зрения взаимодействия с пользователем.
What is frame rate?
Частота смены кадров - это частота, с которой на дисплее появляются последовательные изображения. Все, что мы видим на экранах наших компьютеров, состоит из изображений или кадров, воспроизводимых на экране с такой скоростью, которая воспринимается глазом мгновенно.
Чтобы понять, что это значит, представьте дисплей компьютера в виде флипбука, а страницы флипбука - в виде кадров, воспроизводимых с определенной скоростью, когда вы их переворачиваете.
Для сравнения, компьютерный дисплей - это не что иное, как автоматический флипбук, который воспроизводится непрерывно, когда что-то меняется на экране.
Как правило, чтобы видео воспринималось человеческим глазом плавно и мгновенно, оно должно воспроизводиться со скоростью около 30 кадров в секунду (FPS); чем выше частота, тем лучше качество воспроизведения.
В наши дни большинство устройств обновляют свои экраны со скоростью 60 кадров в секунду, 1/60 = 16,67 мс, что означает, что каждые 16 мс отображается новый кадр. Это число важно, потому что если рендереру React требуется более 16 мс для отображения чего-либо на экране, браузер отбрасывает этот кадр.
Однако на самом деле браузеру приходится выполнять множество других функций, поэтому вся ваша работа должна быть выполнена в течение 10 мс. Если вы не укладываетесь в этот бюджет, частота кадров падает, а содержимое на экране дрожит. Это часто называют перебором, и это негативно сказывается на работе пользователя.
Конечно, это не является большой проблемой для статического и текстового контента. Но в случае отображения анимации это число имеет решающее значение.
Если алгоритм согласования React обходит все дерево приложений каждый раз, когда происходит обновление, повторно отправляет его, и этот обход занимает более 16 мс, кадры будут удаляться.
Это важная причина, по которой многие хотели, чтобы обновления были распределены по приоритетам, а не применялись слепо к каждому обновлению, передаваемому в программу выверки. Кроме того, многие хотели иметь возможность приостанавливать и возобновлять работу в следующем кадре. Таким образом, React мог бы лучше контролировать работу с бюджетом рендеринга в 16 мс.
Это побудило команду React переписать алгоритм согласования, который называется Fiber. Итак, давайте посмотрим, как Fiber работает для решения этой проблемы.
Как работает React Fiber?
Теперь, когда мы знаем, что послужило мотивом для разработки Fiber, давайте подытожим функции, необходимые для ее достижения. И снова я ссылаюсь на заметки Эндрю Кларка.:
- Распределяйте приоритеты между различными видами работ
- Приостановите работу и вернитесь к ней позже
- Прервите работу, если в ней больше нет необходимости
- Повторное использование ранее выполненной работы
Одна из проблем, связанных с реализацией чего-либо подобного, заключается в том, как работает движок JavaScript, и в отсутствии потоков в языке. Чтобы понять это, давайте кратко рассмотрим, как движок JavaScript обрабатывает контексты выполнения.
Стек выполнения JavaScript
Всякий раз, когда вы пишете функцию на JavaScript, движок JavaScript создает контекст выполнения функции.
Каждый раз, когда запускается движок JavaScript, он создает глобальный контекст выполнения, содержащий глобальные объекты; например, объект window в браузере и глобальный объект в Node.js. JavaScript обрабатывает оба контекста, используя стековую структуру данных, также известную как стек выполнения.
Итак, когда вы пишете что-то подобное, движок JavaScript сначала создает глобальный контекст выполнения и помещает его в стек выполнения:
function a() { console.log("i am a") b() } function b() { console.log("i am b") } a()
Затем он создает контекст выполнения функции для функции a(). Поскольку b() вызывается внутри a(), он создает другой контекст выполнения функции для b() и помещает его в стек.
Когда функция b() возвращает результат, обработчик уничтожает контекст функции b(). Когда мы выходим из функции a(), контекст a() уничтожается. Стек во время выполнения выглядит следующим образом:
Но что происходит, когда браузер создает асинхронное событие, такое как HTTP-запрос? Использует ли движок JavaScript стек выполнения и обрабатывает ли асинхронное событие, или он ожидает завершения события?
Движок JavaScript здесь работает по-другому: в верхней части стека выполнения движок JavaScript имеет структуру данных очереди, также известную как очередь событий. Очередь событий обрабатывает асинхронные вызовы, такие как HTTP или сетевые события, поступающие в браузер.
Движок JavaScript обрабатывает элементы в очереди, ожидая, пока стек выполнения опустеет. Таким образом, каждый раз, когда стек выполнения пустеет, движок JavaScript проверяет очередь событий, извлекает элементы из очереди и обрабатывает событие.
Важно отметить, что движок JavaScript проверяет очередь событий только тогда, когда стек выполнения пуст или единственным элементом в стеке выполнения является глобальный контекст выполнения.
Хотя мы называем их асинхронными событиями, здесь есть тонкое различие: события являются асинхронными в отношении того, когда они поступают в очередь, но на самом деле они не являются асинхронными в отношении того, когда они фактически обрабатываются.
Возвращаясь к нашему stack reconciler, когда React обходит дерево, он делает это в стеке выполнения. Таким образом, когда приходят обновления, они попадают в очередь событий (что-то вроде). И только когда стек выполнения пустеет, обновления обрабатываются.
Именно эту проблему решает Fiber, практически полностью обновляя стек интеллектуальными возможностями — например, приостанавливая, возобновляя и прерывая работу.
Опять же, ссылаясь на Эндрю Кларка, “Fiber - это новая реализация стека, специализированная для компонентов React. Вы можете представить себе отдельный fiber как виртуальный стековый фрейм.
“Преимущество переопределения стека заключается в том, что вы можете сохранять стековые фреймы в памяти и выполнять их так, как вам хочется. Это имеет решающее значение для достижения целей, которые мы ставим перед собой при планировании.
“Помимо планирования, ручная работа со стековыми фреймами открывает возможности для таких функций, как параллелизм и определение границ ошибок. Мы рассмотрим эти темы в следующих разделах”.
Проще говоря, волокно представляет собой единицу работы со своим собственным виртуальным стеком. В предыдущей реализации алгоритма согласования React создавал дерево объектов (React elements), которые являются неизменяемыми, и рекурсивно просматривал это дерево.
В текущей реализации React создает дерево узлов fiber, которые могут изменяться. Узел fiber эффективно хранит состояние компонента, реквизиты и базовый элемент DOM, для которого он выполняется.
И, поскольку оптоволоконные узлы могут мутировать, React не нужно пересоздавать каждый узел для обновления; он может просто клонировать и обновлять узел при наличии обновления.
В случае с волоконным деревом React не выполняет рекурсивный обход. Вместо этого он создает односвязный список и выполняет обход сначала по родительскому элементу, затем по глубине.
Односвязный список fiber nodes
fiber nodes представляет собой стековый фрейм и экземпляр компонента React. fiber nodes состоит из следующих элементов:
- Type
- Key
- Child
- Sibling
- Return
- Alternate
- Output
Type
<div> и <span>, например, содержат компоненты (строки), классы или функции для составных компонентов.
Key
Ключ такой же, как и ключ, который мы передаем элементу React.
Child
Представляет элемент, возвращаемый при вызове функции render() для компонента:
const Name = (props) => {
return(
<div className="name">
{props.name}
</div>
)
}
Дочерним элементом <Name> является <div>, поскольку он возвращает элемент <div>.
Sibling
Представляет собой случай, когда render возвращает список элементов:
const Name = (props) => {
return([<Customdiv1 />, <Customdiv2 />])
}
В приведенном выше случае <Customdiv1> и <Customdiv2> являются дочерними элементами <Name>, который является родительским. Эти два дочерних элемента образуют односвязный список.
Return
Return - это возврат обратно к кадру стека, который является логическим возвратом обратно к родительскому оптоволоконному узлу и, таким образом, представляет родительский узел.
pendingProps
and memoizedProps
Запоминание означает сохранение значений результата выполнения функции, чтобы вы могли использовать его позже, тем самым избегая повторных вычислений. pendingProps представляет реквизиты, передаваемые компоненту, а memoizedProps инициализируется в конце стека выполнения, сохраняя реквизиты этого узла.
Когда входящие pendingProps равны memoizedProps, это сигнализирует о том, что предыдущий выходной сигнал оптоволокна можно использовать повторно, предотвращая ненужную работу.
pendingWorkPriority
pendingWorkPriority - это число, указывающее приоритет работы, представленной fiber. В модуле ReactPriorityLevel перечислены различные уровни приоритета и то, что они представляют. За исключением NoWork, который равен нулю, большее число указывает на более низкий приоритет.
Например, вы можете использовать следующую функцию, чтобы проверить, соответствует ли приоритет оптоволокна заданному уровню. Планировщик использует поле приоритет для поиска следующей единицы работы, которую необходимо выполнить:
function matchesPriority(fiber, priority) {
return fiber.pendingWorkPriority !== 0 &&
fiber.pendingWorkPriority <= priority
}
Alternate
В любой момент времени экземпляр компонента имеет не более двух волокон, которые ему соответствуют: текущее волокно и волокно в процессе выполнения. Альтернативный вариант текущего волокна - это волокно в процессе выполнения, а альтернативный вариант текущего волокна - это текущее волокно.
Текущее волокно представляет собой то, что уже отрисовано, а текущее волокно концептуально является кадром стека, который еще не был возвращен.
Output
Результатом являются конечные узлы приложения React. Они специфичны для среды рендеринга (например, в браузерном приложении это div и span). В JSX они обозначаются с помощью имен тегов в нижнем регистре.
Концептуально, выходные данные волокна - это возвращаемое значение функции. Каждое волокно в конечном итоге имеет выходные данные, но выходные данные создаются только в конечных узлах хост-компонентами. Затем выходные данные передаются вверх по дереву.
Выходные данные в конечном итоге передаются программе рендеринга, чтобы она могла внести изменения в среду рендеринга. Например, давайте посмотрим, как выглядит дерево волокон для приложения со следующим кодом:
const Parent1 = (props) => {
return([<Child11 />, <Child12 />])
}
const Parent2 = (props) => {
return(<Child21 />)
}
class App extends Component {
constructor(props) {
super(props)
}
render() {
<div>
<Parent1 />
<Parent2 />
</div>
}
}
ReactDOM.render(<App />, document.getElementById('root'))
Мы можем видеть, что волоконное дерево состоит из односвязных списков дочерних узлов, связанных друг с другом (родственная связь), и связанного списка родительско-дочерних связей. Это дерево можно просмотреть, используя поиск в глубину.
Фаза рендеринга
Чтобы понять, как React строит это дерево и выполняет алгоритм согласования с ним, давайте рассмотрим модульный тест в исходном коде React с подключенным отладчиком, чтобы проследить за процессом; вы можете клонировать исходный код React и перейти в этот каталог.
Для начала добавьте Jest-тест и подключите отладчик. Это простой тест для отображения кнопки с текстом. Когда вы нажимаете на кнопку, приложение уничтожает кнопку и отображает <div> с другим текстом, поэтому текст здесь является переменной состояния:
'use strict';
let React;
let ReactDOM;
describe('ReactUnderstanding', () => {
beforeEach(() => {
React = require('react');
ReactDOM = require('react-dom');
});
it('works', () => {
let instance;
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
text: "hello"
}
}
handleClick = () => {
this.props.logger('before-setState', this.state.text);
this.setState({ text: "hi" })
this.props.logger('after-setState', this.state.text);
}
render() {
instance = this;
this.props.logger('render', this.state.text);
if(this.state.text === "hello") {
return (
<div>
<div>
<button onClick={this.handleClick.bind(this)}>
{this.state.text}
</button>
</div>
</div>
)} else {
return (
<div>
hello
</div>
)
}
}
}
const container = document.createElement('div');
const logger = jest.fn();
ReactDOM.render(<App logger={logger}/>, container);
console.log("clicking");
instance.handleClick();
console.log("clicked");
expect(container.innerHTML).toBe(
'<div>hello</div>'
)
expect(logger.mock.calls).toEqual(
[["render", "hello"],
["before-setState", "hello"],
["render", "hi"],
["after-setState", "hi"]]
);
})
});
При первоначальном рендеринге React создает текущее дерево, которое рендерится изначально.
createFiberFromTypesAndProps() - это функция, которая создает каждое волокно React, используя данные из определенного элемента React. Когда мы запустим тест, поставьте точку останова в этой функции и посмотрите на стек вызовов:
Как мы можем видеть, стек вызовов возвращается к вызову render(), который в конечном итоге переходит к createFiberFromTypeAndProps(). Здесь есть несколько других функций, которые представляют интерес: workLoopSync(), performUnitOfWork() и beginWork().
workLoopSync()
workLoopSync() - это когда React начинает строить дерево, начиная с узла <App> и рекурсивно переходя к <div>, <div> и <button>, которые являются дочерними элементами <App>. Функция WorkInProgress содержит ссылку на следующий оптоволоконный узел, которому предстоит выполнить работу.
performUnitOfWork()
Функция performUnitOfWork() принимает оптоволоконный узел в качестве входного аргумента, получает альтернативный вариант узла и вызывает функцию beginWork(). Это эквивалентно запуску выполнения контекстов выполнения функции в стеке выполнения.
beginWork()
Когда React строит дерево, beginWork() просто приводит к createFiberFromTypeAndProps() и создает узлы fiber. React рекурсивно выполняет работу, и в конечном итоге функция performUnitOfWork() возвращает значение null, указывающее на то, что она достигла конца дерева.
Using instance.handleClick()
Теперь, что происходит, когда мы выполняем instance.handleClick(), который нажимает на кнопку и запускает обновление состояния? В этом случае React обходит дерево волокон, клонирует каждый узел и проверяет, нужно ли выполнять какую-либо работу с каждым узлом.
Когда мы смотрим на стек вызовов этого сценария, он выглядит примерно так:
Хотя мы не видели completeUnitOfWork() и completeWork() в первом стеке вызовов, мы можем увидеть их здесь. Точно так же, как performUnitOfWork() и beginWork(), эти две функции выполняют завершающую часть текущего выполнения, что означает возврат обратно в стек.
Как мы видим, вместе эти четыре функции выполняют единицу работы и обеспечивают контроль над выполняемой в данный момент работой, чего как раз и не хватало в stack reconciler.
На рисунке ниже показано, что каждый оптоволоконный узел состоит из четырех этапов, необходимых для выполнения данной единицы работы.
Здесь важно отметить, что каждый узел не переходит в режим completeUnitOfWork() до тех пор, пока его дочерние узлы не вернут функцию completeWork().
Например, он начинается с performUnitOfWork() и beginWork() для <App/>, затем переходит к performUnitOfWork() и beginWork() для Parent1 и так далее. Он возвращается и завершает работу над <App>, как только все дочерние элементы <App/> завершат работу.
На этом этапе React завершает этап рендеринга. Дерево, которое создается на основе обновления click(), называется деревом WorkInProgress. По сути, это черновое дерево, ожидающее рендеринга.
Фаза Commit
Как только фаза рендеринга завершается, React переходит к фазе фиксации, где он в основном меняет местами корневые указатели текущего дерева и дерева WorkInProgress, тем самым эффективно заменяя текущее дерево на черновое дерево, созданное на основе обновления click().
Более того, React также повторно использует старый current после замены указателя из root на дерево WorkInProgress. Конечным результатом этого оптимизированного процесса является плавный переход от предыдущего состояния приложения к следующему состоянию и далее к следующему состоянию и так далее.
А как насчет времени выполнения кадра в 16 мс? React эффективно запускает внутренний таймер для каждой выполняемой единицы работы и постоянно отслеживает этот временной интервал во время выполнения работы.
В тот момент, когда время истекает, React приостанавливает текущую единицу работы, передает управление обратно основному потоку и позволяет браузеру отображать все, что завершено на данный момент.
Затем, в следующем кадре, React возвращается к тому, на чем остановился, и продолжает построение дерева. Затем, когда у него будет достаточно времени, он фиксирует дерево WorkInProgress и завершает рендеринг.
Обзор изменений и улучшений, внесенных с момента выхода React v16
С момента появления React Fiber в React v16 произошло несколько значительных изменений и улучшений, причем наиболее важные обновления были выпущены в React v18. Эти изменения, в частности, направлены на повышение производительности и удобства работы разработчиков при одновременном решении проблем, связанных с асинхронным рендерингом и параллелизмом.
Вот основные улучшения и изменения, внесенные с момента выхода React v16:
Одновременный рендеринг
Наиболее значительным обновлением React fiber с момента его создания стало обеспечение параллелизма. Это базовое обновление базовой модели рендеринга React, которое использует возможности React Fiber для обеспечения прерывистого рендеринга.
Параллельный режим позволяет React приостанавливать, прерывать или возобновлять рендеринг обновлений в зависимости от приоритета и обрабатывать несколько обновлений одновременно, а не ждать завершения одного обновления перед запуском другого. Это резко отличается от традиционной модели синхронного рендеринга, в которой обновления отображаются в рамках одной непрерывной синхронной транзакции.
Хотя режим параллелизма изначально разрабатывался вместе с архитектурой React Fiber в React v16, он был официально выпущен с React v18 в качестве основного механизма рендеринга и предлагал такие ключевые функции, как:
- Разбиение по времени — это позволяет React разбивать задачи рендеринга на фрагменты и распределять их по нескольким кадрам, предотвращая зависание приложения во время крупных обновлений.
- Выборочное увлажнение — при рендеринге серверного контента на клиенте React теперь может увлажнять только те части пользовательского интерфейса, которые необходимы немедленно.
- Отложенный рендеринг — если React определяет, что обновление не требуется пользователю немедленно, он может отложить рендеринг этого обновления. Это позволяет React сосредоточиться на более важных обновлениях и предотвратить отказ пользовательского интерфейса отвечать на запросы.
Автоматическое дозирование
Автоматическая группировка - ключевая функция, которая является прямым результатом изменений, внесенных в React Fiber. Она помогает сократить количество повторных рендеров, которые происходят при изменении состояния, позволяя React группировать несколько обновлений состояния в один повторный рендеринг.
Например, когда вы вызываете несколько функций setState в одном цикле рендеринга, React автоматически объединяет их в одно обновление. Это можно увидеть в примере кода ниже:
function MyComponent() {
const [count, setCount] = useState(0);
const handleClick = () => {
// Multiple state updates within a single event handler
setCount(count + 1);
setCount(count + 2);
};
console.log("Rendering")
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
Функция handleClick вызывает setCount дважды, один раз, чтобы увеличить значение на 1, а затем еще раз, чтобы увеличить его на 2.
React автоматически объединяет эти два обновления состояния в одно обновление. Это означает, что будет выполнен только один повторный рендеринг, а “рендеринг” будет зарегистрирован в терминале только один раз.
До выпуска React v18 пакетные обновления выполнялись внутри обработчика событий React. Это означало, что за пределами жизненного цикла React (например, внутри setTimeout или promise) обновления не будут загружаться автоматически. Благодаря улучшенному обновлению React Fiber в версии 18 обновления передаются пакетно во всех контекстах, включая асинхронный код, setTimeout, promises и собственные обработчики событий.
Неизвестность
Suspense - это функция, тесно связанная с параллельным режимом и первоначально появившаяся в React fiber в версии 16, но ее возможности были значительно расширены с обновлениями в React v17 и v18.
С момента своего первоначального выпуска эта функция претерпела значительные улучшения. Изначально она была разработана для управления отложенной загрузкой (разделением кода) компонентов с помощью React.lazy, но ее функциональность расширилась, особенно с появлением параллельного режима в версии 18.
Теперь Suspense играет ключевую роль в обработке асинхронных операций, таких как выборка данных, и обеспечивает более детальный контроль над отображением пользовательского интерфейса во время этих операций. Вы можете декларативно указать, что должен показывать React, когда часть дерева еще не готова к отображению.
Это делается с помощью запасного варианта, который отображает компонент или строку во время выполнения асинхронной задачи:
<Suspense fallback={<div>Loading data...</div>}>
<SomeComponent />
</Suspense>
Другим ключевым улучшением является потоковая передача при рендеринге на стороне сервера (SSR) и выборочная гидратация. Благодаря этому усовершенствованию сервер может отправлять клиенту исходную облегченную HTML-оболочку и обрабатывать содержимое порциями по мере поступления данных.
React может выборочно обновлять только те части приложения, которые сразу видны пользователю, а остальные приостанавливать до тех пор, пока не будут готовы необходимые данные или ресурсы.
Переходы
Transitions - это новый API, созданный поверх React Fiber, который использует свой алгоритм планирования для распознавания и пометки обновлений как срочных или несрочных.
К срочным обновлениям относятся такие задачи, как ввод данных пользователем или анимация, при выполнении которых ожидается немедленная обратная связь. Несрочные обновления, такие как отображение списка результатов поиска, имеют низкий приоритет, и допустимы небольшие задержки.
API transitions содержит функцию useTransition, которая позволяет помечать обновления состояния как несрочные, поскольку каждое не отмеченное обновление состояния по умолчанию считается срочным.
Переход к использованию
Перехватчик useTransition обрабатывает переходы между состояниями пользовательского интерфейса неблокирующим образом. Он возвращает массив с двумя значениями:
- isPending — логическое значение, указывающее, выполняется ли переход.
- startTransition — функция для запуска перехода
Чтобы понять, как работает этот механизм, представьте компонент, отображающий длинный список элементов, которые можно отфильтровать. Без useTransition обновление фильтра может привести к задержке работы, поскольку React повторно отображает длинный список при каждом изменении фильтра.
С помощью useTransition мы можем пометить обновление, которое отображает отфильтрованный список, как несрочное, сохраняя при этом оперативность пользовательского ввода:
import React, { useState, useTransition } from 'react';
const FilteredList = ({ items }) => {
const [filter, setFilter] = useState('');
const [filteredItems, setFilteredItems] = useState(items);
const [isPending, startTransition] = useTransition();
const handleFilterChange = (e) => {
const value = e.target.value;
setFilter(value);
// Mark this update as non-urgent
startTransition(() => {
const updatedItems = items.filter(item =>
item.toLowerCase().includes(value.toLowerCase())
);
setFilteredItems(updatedItems);
});
};
return (
<div>
<input
type="text"
value={filter}
onChange={handleFilterChange}
placeholder="Filter items"
/>
{isPending && <p>Updating list...</p>}
<ul>
{filteredItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
};
export default FilteredList;
В этом примере setFilter обновляется немедленно, поскольку пользователю необходимо срочно увидеть отражение своего ввода во входных данных, в то время как setfiltereditems добавляется в startTransition, позволяя React обновлять отфильтрованный список, когда система будет готова.
Функция isPending используется для отображения сообщения о загрузке, когда процесс фильтрации занимает некоторое время. Это поможет улучшить взаимодействие с пользователем, предоставляя обратную связь, пока список обновляется в фоновом режиме.
Новые API-интерфейсы рендеринга
В React v18 появились новые клиентские API рендеринга, createRoot и hydrateRoot, которые предлагают лучший способ обработки и рендеринга корневых узлов. createRoot заменяет предыдущий React.render API и необходим для включения новых функций, таких как параллелизм. Улучшения, описанные в предыдущем и последующих разделах, без этого работать не будут.
createRoot, как и его предшественник, помогает React подключаться к DOM и управлять им изнутри. Он создает root для узла DOM и привязывает его к узлу React, такому как компонент верхнего уровня, например <App>, или элемент React, созданный с помощью createElement:
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
API hydrateRoot является SSR-эквивалентом API createRoot. Он заменяет React.hydrate и позволяет отображать компоненты React внутри DOM-узла браузера, HTML-содержимое которого ранее было сгенерировано в серверной среде:
import { hydrateRoot } from 'react-dom/client';
const root = hydrateRoot(document.getElementById('root'));
root.render(<App />);
Как показано в примерах кода, оба API возвращают объект с помощью метода render. Эти методы служат одной и той же цели - рендеринга компонентов React в DOM, но ведут себя немного по-разному.
Метод render в createRoot создает новый корневой экземпляр для приложения React, что означает, что дерево компонентов будет отображаться с нуля. В отличие от этого, метод render в hydrateRoot только увлажняет существующую структуру DOM, ранее отображенную на сервере.
Возвращаемый объект также включает в себя второй метод, unmount, который используется для уничтожения отображаемого дерева внутри корня React. Приложениям, полностью созданным с использованием React, обычно не требуется вызывать unmount; в основном это касается стороннего кода, которому необходимо размонтировать все компоненты в корневом каталоге и отсоединить React от корневого узла DOM.
Команда React упростила переход на новый клиентский API рендеринга, позволив пользователям продолжать использовать существующие API в React 17. Это означает, что существующие приложения React 17 могут быть постепенно перенесены на React v18 без существенных изменений.
Новые усовершенствованные хуки
В React v18 также появились новые перехватчики: useInsertionEffect и useSyncExternalStore, которые служат конкретным целям, связанным с оптимизацией производительности и решением задач, которые не были полностью обработаны существующими перехватчиками, такими как useEffect или useLayoutEffect.
Использует syncexternalstore
Функция useSyncExternalStore позволяет синхронизировать ваши компоненты с внешними источниками данных, которые не соответствуют жизненному циклу обновления React. Эта функция обеспечивает согласованность между внутренним состоянием React и внешними хранилищами (такими как библиотеки управления состоянием), особенно при одновременном рендеринге.
useSyncExternalStore был специально разработан для решения проблем, связанных с условиями гонки, которые возникают, когда состояние внешнего хранилища изменяется на этапе рендеринга, что приводит к несоответствиям между состоянием React reads и состоянием, которое оно использует для рендеринга компонентов.
Без useSyncExternalStore библиотеки обычно полагаются на useEffect и useLayoutEffect для подписки на внешние хранилища и запуска повторного отображения при изменении хранилища.
Однако в среде одновременного рендеринга это может вызвать проблемы, поскольку useEffect запускается после фазы фиксации, что означает, что React может завершить рендеринг до обнаружения каких-либо изменений во внешнем состоянии, что потенциально может привести к устареванию или несогласованности данных.
Аналогично, при использовании useLayoutEffect, который запускается перед отрисовкой браузером, React все равно может зафиксировать рендеринг на основе устаревшего внешнего состояния, поскольку состояние могло измениться в процессе рендеринга React.
Чтобы узнать больше о подключении useSyncExternalStore, обратитесь к этой статье.
Используйте эффект useInsertEffect
Хук useInsertEffect предназначен для внедрения стилей или выполнения манипуляций с DOM до того, как React внесет какие-либо изменения в DOM. Это особенно полезно для библиотек CSS-in-JS, таких как Emotion и styled components, которым необходимо убедиться, что стили вставлены перед этапом рендеринга, чтобы избежать мерцания или несоответствия стилей.
Вывод
Мы надеемся, вам понравилось читать этот пост. Пожалуйста, не стесняйтесь оставлять комментарии или вопросы, если у вас таковые возникнут.