跳到主要内容

渲染、提交和挂载

注意

本文档引用了新架构,该架构正在积极推广中。

React Native 渲染器通过一系列工作将 React 逻辑渲染到宿主平台(Host Platform)。这一系列工作被称为渲染管线(render pipeline),它在初始渲染和 UI 状态更新时触发。本文将详细介绍渲染管线,并探讨其在不同场景下的差异。

渲染管线可大致分为三个阶段:

  1. 渲染(Render):React 执行业务逻辑,在 JavaScript 中创建 React 元素树(React Element Tree)。渲染器基于此树在 C++ 中创建一个 React 投影树(React Shadow Tree)
  2. 提交(Commit):在 React 投影树完整创建后,渲染器会触发提交。这会将 React 元素树和新创建的 React 投影树提升(promote)为即将挂载的“下一棵树”。同时,它还会调度布局信息的计算。
  3. 挂载(Mount):包含布局计算结果的 React 投影树被转换为宿主视图树(Host View Tree)

渲染管线的不同阶段可能在不同的线程上执行。更多详情请参阅线程模型(Threading Model)文档。

React Native renderer Data flow


初始渲染

设想你想要渲染以下内容:

React JSX
function MyComponent() {
return (
<View>
<Text>Hello, World</Text>
</View>
);
}

// <MyComponent />

在上述示例中,<MyComponent /> 是一个 React 元素。React 通过递归调用它(如果是基于 JavaScript 类的实现,则调用其 render 方法),将该 React 元素缩减为最终的 React 宿主组件(React Host Component),直到所有的 React 元素都无法进一步缩减为止。现在,你就拥有了一棵由 React 宿主组件组成的 React 元素树

阶段 1. 渲染

Phase one: render

在元素缩减的过程中,每当一个 React 元素被调用时,渲染器也会同步创建一个 React 投影节点(React Shadow Node)。这仅针对 React 宿主组件发生,而不针对 React 复合组件。在上述示例中,<View> 导致创建了一个 ViewShadowNode 对象,<Text> 导致创建了一个 TextShadowNode 对象。值得注意的是,不存在直接代表 <MyComponent> React 投影节点

每当 React 在两个 React 元素节点之间创建父子关系时,渲染器也会在对应的 React 投影节点之间创建相同的关系。这就是 React 投影树的组装方式。

更多细节

  • 这些操作(创建 React 投影节点、创建两个 React 投影节点之间的父子关系)是同步且线程安全的操作,它们从 React (JavaScript) 执行到渲染器 (C++) 中,通常在 JavaScript 线程上完成。
  • React 元素树(及其构成的 React 元素节点)不会永久存在。它是由 React 中的“纤维(fibers)”物化而成的临时表现形式。每个代表宿主组件的“fiber”都存储一个指向 React 投影节点的 C++ 指针,这是通过 JSI 实现的。在此文档中详细了解“fibers”。
  • React 投影树是不可变的。为了更新任何 React 投影节点,渲染器会创建一个新的 React 投影树。然而,渲染器提供了克隆操作,以使状态更新更具性能优势(详见 React 状态更新)。

在上述示例中,渲染阶段的结果如下所示:

Step one

React 投影树完成后,渲染器会触发 React 元素树的提交。

阶段 2. 提交

Phase two: commit

提交阶段包含两个操作:布局计算(Layout Calculation)树提升(Tree Promotion)

  • 布局计算:该操作计算每个 React 投影节点的位置和大小。在 React Native 中,这涉及调用 Yoga 来计算每个 React 投影节点的布局。实际计算需要每个 React 投影节点的样式(源自 JavaScript 中的 React 元素)。它还需要 React 投影树根节点的布局约束,这决定了结果节点可以占据的可用空间大小。

Step two

  • 树提升(新树 → 下一棵树):该操作将新的 React 投影树提升为即将挂载的“下一棵树”。此提升表明新的 React 投影树已具备挂载所需的所有信息,并代表了 React 元素树的最新状态。“下一棵树”将在 UI 线程的下一个“tick”进行挂载。

更多细节

  • 这些操作在后台线程上异步执行。
  • 大部分布局计算完全在 C++ 内部执行。然而,某些组件的布局计算依赖于宿主平台(例如 TextTextInput 等)。文本的大小和位置特定于每个宿主平台,需要在宿主平台层进行计算。因此,Yoga 会调用宿主平台中定义的函数来计算该组件的布局。

阶段 3. 挂载

Phase three: mount

挂载阶段将 React 投影树(现已包含来自布局计算的数据)转换为屏幕上显示渲染像素的宿主视图树。作为回顾,React 元素树如下所示:

React JSX
<View>
<Text>Hello, World</Text>
</View>

从宏观上看,React Native 渲染器为每个 React 投影节点创建一个对应的宿主视图(Host View)并将其挂载到屏幕上。在上述示例中,渲染器为 <View> 创建 android.view.ViewGroup 的实例,为 <Text> 创建 android.widget.TextView,并将其填充为“Hello World”。同样,在 iOS 上会创建一个 UIView,并通过调用 NSLayoutManager 来填充文本。然后,每个宿主视图都被配置为使用其 React 投影节点的 props,并使用计算出的布局信息配置其大小和位置。

Step two

具体来说,挂载阶段包含以下三个步骤:

  • 树差异计算(Tree Diffing):此步骤完全在 C++ 中计算“先前渲染的树”与“下一棵树”之间的差异。结果是一个要在宿主视图上执行的原子突变操作列表(例如 createViewupdateViewremoveViewdeleteView 等)。这一步也是 React 投影树进行平铺(flattening)的地方,以避免创建不必要的宿主视图。有关此算法的详细信息,请参阅视图平铺(View Flattening)
  • 树提升(下一棵树 → 已渲染树):此步骤原子地将“下一棵树”提升为“先前渲染的树”,以便下一次挂载阶段针对正确的树计算差异。
  • 视图挂载:此步骤将原子突变操作应用于相应的宿主视图。此步骤在 UI 线程上的宿主平台中执行。

更多细节

  • 这些操作在 UI 线程上同步执行。如果提交阶段在后台线程执行,挂载阶段将被调度到 UI 线程的下一个“tick”。另一方面,如果提交阶段在 UI 线程上执行,挂载阶段会在同一线程上同步执行。
  • 挂载阶段的调度、实现和执行在很大程度上取决于宿主平台。例如,Android 和 iOS 之间的挂载层渲染器架构目前有所不同。
  • 在初始渲染期间,“先前渲染的树”为空。因此,树差异计算步骤的结果将仅包含创建视图、设置 props 以及将视图相互添加的操作列表。在处理 React 状态更新时,树差异计算对于性能变得更加重要。
  • 在当前的生产测试中,一个 React 投影树通常包含约 600-1000 个 React 投影节点(视图平铺前),平铺后树被缩减为约 200 个节点。在 iPad 或桌面应用中,此数量可能会增加 10 倍。

React 状态更新

让我们探索当 React 元素树的状态更新时渲染管线的每个阶段。假设你在初始渲染中渲染了以下组件:

React JSX
function MyComponent() {
return (
<View>
<View
style={{backgroundColor: 'red', height: 20, width: 20}}
/>
<View
style={{backgroundColor: 'blue', height: 20, width: 20}}
/>
</View>
);
}

应用在初始渲染部分描述的内容,你可以预期会创建以下树:

Render pipeline 4

注意,节点 3 映射到一个带有红色背景的宿主视图,节点 4 映射到一个带有蓝色背景的宿主视图。假设作为 JavaScript 业务逻辑中状态更新的结果,第一个嵌套 <View> 的背景从 'red' 变为 'yellow'。新的 React 元素树可能如下所示:

React JSX
<View>
<View
style={{backgroundColor: 'yellow', height: 20, width: 20}}
/>
<View
style={{backgroundColor: 'blue', height: 20, width: 20}}
/>
</View>

React Native 如何处理此更新?

当发生状态更新时,渲染器需要从概念上更新 React 元素树,以便更新已挂载的宿主视图。但为了保持线程安全, React 元素树 React 投影树都必须是不可变的。这意味着 React 不会突变当前的 React 元素树 React 投影树,而是必须创建每棵树的新副本,其中包含新的 props、样式和子节点。

让我们探索状态更新期间渲染管线的每个阶段。

阶段 1. 渲染

Phase one: render

当 React 创建包含新状态的新 React 元素树时,它必须克隆受更改影响的每个 React 元素 React 投影节点。克隆后,新的 React 投影树被提交。

React Native 渲染器利用结构共享来最小化不可变性的开销。当为了包含新状态而克隆 React 元素时,路径上直到根节点的所有 React 元素都会被克隆。只有当 React 元素需要更新其 props、样式或子节点时,React 才会克隆它。任何未受状态更新影响的 React 元素都由旧树和新树共享。

在上面的示例中,React 使用这些操作创建新树:

  1. CloneNode(节点 3, {backgroundColor: 'yellow'}) → 节点 3'
  2. CloneNode(节点 2) → 节点 2'
  3. AppendChild(节点 2', 节点 3')
  4. AppendChild(节点 2', 节点 4)
  5. CloneNode(节点 1) → 节点 1'
  6. AppendChild(节点 1', 节点 2')

这些操作之后,节点 1' 代表了新 React 元素树的根。让我们将 T 分配给“先前渲染的树”,将 T' 分配给“新树”:

Render pipeline 5

注意 TT' 如何共享节点 4。结构共享提高了性能并减少了内存使用。

阶段 2. 提交

Phase two: commit

React 创建新的 React 元素树 React 投影树后,必须提交它们。

  • 布局计算:类似于初始渲染期间的布局计算。一个重要的区别是布局计算可能会导致共享的 React 投影节点被克隆。这是因为如果共享的 React 投影节点的父级发生了布局更改,共享的 React 投影节点的布局也可能发生更改。
  • 树提升(新树 → 下一棵树):类似于初始渲染期间的树提升。

阶段 3. 挂载

Phase three: mount

  • 树提升(下一棵树 → 已渲染树):此步骤原子地将“下一棵树”提升为“先前渲染的树”,以便下一次挂载阶段针对正确的树计算差异。
  • 树差异计算:此步骤计算“先前渲染的树”(T)与“下一棵树”(T')之间的差异。结果是需要在宿主视图上执行的原子突变操作列表。
    • 在上述示例中,操作包括:UpdateView(**节点 3**, {backgroundColor: 'yellow'})
    • 差异计算可以针对任何当前已挂载的树与任何新树进行。渲染器可以跳过树的一些中间版本。
  • 视图挂载:此步骤将原子突变操作应用于相应的宿主视图。在上述示例中,只有视图 3backgroundColor 会被更新(变为黄色)。

Render pipeline 6


React Native 渲染器状态更新

对于投影树中的大多数信息,React 是单一所有者和单一事实来源。所有数据源自 React,并且存在单一方向的数据流。

然而,有一个例外和重要的机制:C++ 中的组件可以包含未直接暴露给 JavaScript 的状态,且 JavaScript 并非事实来源。C++ 和宿主平台控制此 C++ 状态。通常,这仅在你正在开发需要 C++ 状态的复杂宿主组件时才相关。绝大多数宿主组件不需要此功能。

例如,ScrollView 使用此机制告知渲染器当前的偏移量。该更新由宿主平台触发,具体来说是从代表 ScrollView 组件的宿主视图触发。关于偏移量的信息被用于类似 measure 的 API 中。由于此更新源于宿主平台,且不影响 React 元素树,因此此状态数据由 C++ 状态持有。

从概念上讲, C++ 状态更新类似于上述描述的 React 状态更新。但有两个重要区别:

  1. 它们跳过了“渲染阶段”,因为不涉及 React。
  2. 更新可以在任何线程上发起和发生,包括主线程。

阶段 2. 提交

Phase two: commit

执行 C++ 状态更新时,一段代码请求更新 ShadowNode (N) 以将 C++ 状态设置为值 S。React Native 渲染器将反复尝试获取最新提交版本的 N,用新状态 S 克隆它,并将 N’ 提交到树中。如果在此期间 React 或另一个 C++ 状态更新进行了另一次提交,则 C++ 状态提交将失败,渲染器将多次重试 C++ 状态更新,直到提交成功。这防止了事实来源的冲突和竞争。

阶段 3. 挂载

Phase three: mount

挂载阶段实际上与 React 状态更新的挂载阶段几乎相同。渲染器仍然需要重新计算布局、执行树差异计算等。详情请见上述章节。