Перейти к содержанию

Render, Commit и Mount

Рендерер React Native проходит последовательность работ для рендеринга логики React на хост-платформу. Эта последовательность работ называется конвейером рендеринга и выполняется для начального рендеринга и обновления состояния пользовательского интерфейса. В этом документе рассматривается конвейер рендеринга и его различия в этих сценариях.

Конвейер рендеринга можно разбить на три общие фазы:

  1. Рендер: React выполняет логику продукта, которая создает React Element Trees на JavaScript. Из этого дерева рендерер создает React Shadow Tree на C++.
  2. Коммит: После полного создания React Shadow Tree рендерер запускает коммит. Это продвигает как React Element Tree, так и вновь созданное React Shadow Tree в качестве "следующего дерева", которое будет смонтировано. Это также планирует вычисление информации о его компоновке.
  3. Монтаж: Дерево теней React, теперь с результатами расчета компоновки, преобразуется в Host View Tree.

Фазы конвейера рендеринга могут происходить в разных потоках. Более подробную информацию см. в документе Threading Model doc.

React Native renderer Data flow

Начальный рендеринг

Представьте, что вы хотите отобразить следующее:

1
2
3
4
5
6
7
8
9
function MyComponent() {
    return (
        <View>
            <Text>Hello, World</Text>
        </View>
    );
}

// <MyComponent />

В приведенном выше примере <MyComponent /> является React Element. React рекурсивно уменьшает этот React-элемент до конечного React Host-компонента, вызывая его (или его метод render, если он реализован с помощью класса JavaScript), пока каждый React-элемент не сможет быть уменьшен еще больше. Теперь у вас есть дерево React-элементов React Host Components.

Фаза 1. Рендеринг

Первая фаза: рендеринг

Во время этого процесса сокращения элементов, когда вызывается каждый React Element, рендерер также синхронно создает React Shadow Node. Это происходит только для React Host Components, но не для React Composite Components. В приведенном выше примере <View> приводит к созданию объекта ViewShadowNode, а объект <Text> приводит к созданию объекта TextShadowNode. Примечательно, что никогда не существует React Shadow Node, который непосредственно представляет <МойКомпонент>.

Всякий раз, когда React создает отношения родитель-ребенок между двумя React Element Nodes, рендерер создает такие же отношения между соответствующими React Shadow Nodes. Так собирается React Shadow Tree.

Дополнительные сведения

  • Операции (создание React Shadow Node, создание отношений родитель-ребенок между двумя React Shadow Node) являются синхронными и потокобезопасными операциями, которые выполняются из React (JavaScript) в рендерер (C++), обычно в потоке JavaScript.
  • Дерево React Element Tree (и составляющие его React Element Nodes) не существует бесконечно. Это временное представление, материализованное "волокнами" в React. Каждое "волокно", представляющее компонент узла, хранит указатель C++ на React Shadow Node, что стало возможным благодаря JSI. Подробнее о "волокнах" в этом документе.
  • Дерево React Shadow Tree является неизменяемым. Чтобы обновить любой React Shadow Node, рендерер создает новое React Shadow Tree. Однако рендерер предоставляет операции клонирования, чтобы сделать обновление состояния более производительным (подробнее см. в React State Updates).

В приведенном выше примере результат фазы рендеринга выглядит следующим образом:

Шаг первый

После завершения работы React Shadow Tree рендерер запускает фиксацию React Element Tree.

Фаза 2. Коммит

Вторая фаза: фиксация

Фаза фиксации состоит из двух операций: Расчет макета и Продвижение дерева.

  • Расчет макета: Эта операция вычисляет положение и размер каждого React Shadow Node. В React Native это включает вызов Yoga для расчета расположения каждого React Shadow Node. Для фактического расчета требуются стили каждого React Shadow Node, которые берутся из React Element в JavaScript. Для этого также требуются ограничения компоновки корня React Shadow Tree, которые определяют количество свободного пространства, которое могут занимать результирующие узлы.

Шаг второй

  • Продвижение дерева (Новое дерево → Следующее дерево): Эта операция продвигает новое React Shadow Tree в качестве "следующего дерева", которое должно быть смонтировано. Это продвижение указывает на то, что новое React Shadow Tree имеет всю информацию для монтирования и представляет собой последнее состояние React Element Tree. Следующее дерево" монтируется при следующем "тике" UI Thread.

Дополнительные сведения

  • Эти операции выполняются асинхронно в фоновом потоке.
  • Большинство расчетов компоновки выполняется полностью в C++. Однако, расчет компоновки некоторых компонентов зависит от хост-платформы (например, Text, TextInput и т.д.). Размер и положение текста специфичны для каждой хост-платформы и должны быть рассчитаны на уровне хост-платформы. Для этого Yoga вызывает функцию, определенную в хост-платформе, чтобы рассчитать расположение компонента.

Фаза 3. Монтирование

Третья фаза: монтирование

Фаза монтажа преобразует React Shadow Tree (которое теперь содержит данные из расчета компоновки) в Host View Tree с отрисованными пикселями на экране. В качестве напоминания, React Element Tree выглядит следующим образом:

1
2
3
<View>
    <Text>Hello, World</Text>
</View>

На высоком уровне рендерер React Native создает соответствующий Host View для каждого React Shadow Node и монтирует его на экран. В приведенном выше примере рендерер создает экземпляр android.view.ViewGroup для <View> и android.widget.TextView для <Text> и заполняет его текстом "Hello World". Аналогично для iOS создается UIView и текст заполняется вызовом NSLayoutManager. Затем каждое представление узла настраивается на использование props из своего узла React Shadow, а его размер и положение настраиваются с помощью рассчитанной информации о макете.

Шаг второй

Более подробно, этап монтажа состоит из следующих трех шагов:

  • Дифференцирование деревьев: Этот шаг вычисляет разницу между "ранее отрисованным деревом" и "следующим деревом" полностью на C++. Результатом является список атомарных операций мутации, которые должны быть выполнены над представлениями хоста (например, createView, updateView, removeView, deleteView и т.д.). На этом этапе также происходит сглаживание дерева теней React Shadow Tree, чтобы избежать создания ненужных хост-представлений. Подробности об этом алгоритме смотрите в View Flattening.
  • Продвижение дерева (следующее дерево → рендерированное дерево): Этот шаг атомарно продвигает "следующее дерево" к "ранее отрендеренному дереву", чтобы на следующем этапе монтирования вычислить diff относительно соответствующего дерева.
  • Видовой монтаж: Этот шаг применяет атомарные операции мутации к соответствующим представлениям хоста. Этот шаг выполняется на хостовой платформе в потоке UI.

Дополнительные сведения

  • Операции выполняются синхронно в потоке UI. Если фаза фиксации выполняется в фоновом потоке, то фаза монтажа запланирована на следующий "тик" потока UI. С другой стороны, если фаза фиксации выполняется в потоке UI, фаза монтажа выполняется синхронно в том же потоке.
  • Планирование, реализация и выполнение фазы монтажа сильно зависит от хостовой платформы. Например, архитектура рендеринга монтажного слоя в настоящее время различается между Android и iOS.
  • Во время начального рендеринга "ранее отрендеренное дерево" пусто. Поэтому шаг диффиринга дерева приведет к списку операций мутации, состоящему только из создания представлений, установки пропсов и добавления представлений друг к другу. Дифференциация деревьев становится более важной для производительности при обработке React State Updates.
  • В текущих производственных тестах React Shadow Tree обычно состоит примерно из 600-1000 React Shadow Nodes (до сглаживания представлений), после сглаживания представлений деревья сокращаются до ~200 узлов. На iPad или в настольных приложениях это количество может увеличиться в 10 раз.

Обновления состояния React

Давайте рассмотрим каждую фазу конвейера рендеринга, когда обновляется состояние React Element Tree. Допустим, вы отобразили следующий компонент в начальном рендере:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function MyComponent() {
    return (
        <View>
            <View
                style={{
                    backgroundColor: 'red',
                    height: 20,
                    width: 20,
                }}
            />
            <View
                style={{
                    backgroundColor: 'blue',
                    height: 20,
                    width: 20,
                }}
            />
        </View>
    );
}

Применяя то, что было описано в разделе Initial Render, можно ожидать создания следующих деревьев:

Render pipeline 4

Обратите внимание, что узел 3 отображается на представление узла с красным фоном, а узел 4 — на представление узла с синим фоном. Предположим, что в результате обновления состояния в логике продукта JavaScript фон первого вложенного <View> меняется с красного на желтый. Вот как может выглядеть новое дерево элементов React Element Tree:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<View>
    <View
        style={{
            backgroundColor: 'yellow',
            height: 20,
            width: 20,
        }}
    />
    <View
        style={{
            backgroundColor: 'blue',
            height: 20,
            width: 20,
        }}
    />
</View>

Как это обновление обрабатывается в React Native?

Когда происходит обновление состояния, рендерер должен концептуально обновить React Element Tree, чтобы обновить уже смонтированные представления хоста. Но чтобы сохранить безопасность потоков, как React Element Tree, так и React Shadow Tree должны быть неизменяемыми. Это означает, что вместо изменения текущего React Element Tree и React Shadow Tree, React должен создать новую копию каждого дерева, которая будет включать в себя новые пропсы, стили и дочерние элементы.

Давайте рассмотрим каждый этап конвейера рендеринга во время обновления состояния.

Фаза 1. Рендеринг

Первая фаза: рендеринг

Когда React создает новое React Element Tree, включающее новое состояние, он должен клонировать каждый React Element и React Shadow Node, на которые повлияло изменение. После клонирования новое React Shadow Tree фиксируется.

Рендерер React Native использует структурное разделение для минимизации накладных расходов на неизменяемость. Когда React-элемент клонируется для включения нового состояния, клонируется каждый React-элемент, находящийся на пути к корню. React клонирует React-элемент, только если требуется обновление его пропсов, стиля или дочерних элементов. Любые React-элементы, которые не изменились в результате обновления состояния, разделяются между старым и новым деревьями.

В приведенном выше примере React создает новое дерево с помощью этих операций:

  1. CloneNode(Node 3, {backgroundColor: 'yellow'}) → Node 3'.
  2. CloneNode(Node 2) → Node 2'.
  3. AppendChild(Узел 2', Узел 3')
  4. AppendChild(Узел 2', Узел 4)
  5. CloneNode(Node 1) → Node 1'
  6. AppendChild(Узел 1', Узел 2')

После этих операций узел 1' представляет собой корень нового дерева React Element Tree. Давайте присвоим T "ранее отрисованному дереву" и T' "новому дереву":

Render pipeline 5

Обратите внимание, что T и T' оба используют узел 4. Совместное использование структур повышает производительность и снижает использование памяти.

Фаза 2. Коммит

Вторая фаза: фиксация

После того как React создаст новые React Element Tree и React Shadow Tree, он должен зафиксировать их.

  • Расчет макета: Аналогичен расчету макета во время Initial Render. Важным отличием является то, что расчет макета может привести к клонированию общих React Shadow Nodes. Это может произойти потому, что если родитель общего React Shadow Node изменит расположение, расположение общего React Shadow Node также может измениться.
  • Продвижение дерева (новое дерево → следующее дерево): Аналогично продвижению дерева во время Initial Render.
  • Дифференцирование деревьев: Этот шаг вычисляет разницу между "ранее отрендеренным деревом" (T) и "следующим деревом" (T'). Результатом является список атомарных мутационных операций, которые должны быть выполнены над хостовыми представлениями.

    • В приведенном выше примере операции состоят из: UpdateView(**Node 3'**, {backgroundColor: ''yellow'}).

Фаза 3. Монтирование

Третья фаза: монтирование

  • Продвижение дерева (следующее дерево → рендерированное дерево): Этот шаг атомарно переводит "следующее дерево" в "ранее отрендеренное дерево", чтобы на следующей фазе монтирования вычислить разницу с соответствующим деревом. Diff может быть вычислен для любого смонтированного в данный момент дерева с любым новым деревом. Рендерер может пропустить некоторые промежуточные версии дерева.
  • Монтирование вида: Этот этап применяет атомарные операции мутации к соответствующим хостовым представлениям. В приведенном выше примере только backgroundColor вида View 3 будет обновлен (на желтый).

Render pipeline 6

Обновления состояния рендерера React Native

Для большинства информации в Shadow Tree, React является единственным владельцем и единственным источником правды. Все данные исходят из React, и поток данных идет в одном направлении.

Однако есть одно исключение и важный механизм: компоненты на C++ могут содержать состояние, которое не подвергается прямому воздействию JavaScript, и JavaScript не является источником истины. C++ и Host Platform контролируют это C++ State. Как правило, это актуально только в том случае, если вы разрабатываете сложный Host-компонент, которому требуется C++ State. Подавляющему большинству Host-компонентов эта функциональность не нужна.

Например, ScrollView использует этот механизм, чтобы сообщить рендереру о текущем смещении. Обновление запускается с хостовой платформы, в частности, с хостового представления, которое представляет компонент ScrollView. Информация о смещении используется в API типа measure. Поскольку это обновление исходит от платформы хоста и не влияет на дерево элементов React, эти данные о состоянии хранятся в C++ State.

Концептуально обновления C++ State похожи на описанные выше React State Updates. С двумя важными отличиями:

  1. Они пропускают "фазу рендеринга", поскольку React не задействован.
  2. Обновления могут возникать и происходить в любом потоке, включая основной поток.

Фаза 2. Коммит

Вторая фаза: фиксация

При выполнении обновления C++ State блок кода запрашивает обновление ShadowNode (N) для установки C++ State в значение S. Рендерер React Native будет неоднократно пытаться получить последнюю зафиксированную версию N, клонировать ее с новым состоянием S и зафиксировать N' в дереве. Если за это время React или другое обновление C++ State выполнило другую фиксацию, фиксация C++ State будет неудачной, и рендерер будет повторять попытку обновления C++ State много раз, пока фиксация не будет успешной. Это предотвращает столкновения и гонки источников истинности.

Фаза 3. Монтирование

Третья фаза: монтирование

Фаза Mount Phase практически идентична Mount Phase of React State Updates. Рендерер все еще должен пересчитать компоновку, выполнить разделение дерева и т.д. Подробности см. в разделах выше.

Комментарии