渲染、提交和挂载
本文档引用了正在积极推行中的新架构。
React Native 渲染器执行一系列工作,将 React 逻辑渲染到宿主平台。这一系列工作称为渲染管道,发生在初始渲染和 UI 状态更新时。本文档将介绍渲染管道及其在这些场景中的区别。
渲染管道可以分为三个通用阶段:
- 渲染:React 执行产品逻辑,在 JavaScript 中创建React 元素树。渲染器从该树中在 C++ 中创建React 影子树。
- 提交:当 React 影子树完全创建后,渲染器触发提交。这会提升React 元素树和新创建的 React 影子树作为“下一个待挂载的树”。这还会安排计算其布局信息。
- 挂载:React 影子树,现在包含布局计算的结果,被转换为宿主视图树。
渲染管道的各个阶段可能在不同的线程上发生。有关更多详细信息,请参阅线程模型文档。

初始渲染
想象一下你想渲染以下内容:
function MyComponent() {
return (
<View>
<Text>Hello, World</Text>
</View>
);
}
// <MyComponent />
在上面的示例中,<MyComponent /> 是一个React 元素。React 递归地将这个React 元素减少为终端React 宿主组件,方法是调用它(或者如果用 JavaScript 类实现,则调用其render方法),直到每个React 元素不能再进一步减少。现在你有一个由React 宿主组件组成的React 元素树。
阶段 1. 渲染

在元素缩减过程中,当每个React 元素被调用时,渲染器也会同步创建一个React 影子节点。这仅发生在React 宿主组件上,而不是React 复合组件上。在上面的示例中,<View> 导致创建了一个 ViewShadowNode 对象,而 <Text> 导致创建了一个 TextShadowNode 对象。值得注意的是,从来没有一个React 影子节点直接代表 <MyComponent>。
每当 React 在两个React 元素节点之间创建父子关系时,渲染器会在相应的React 影子节点之间创建相同的关系。这就是React 影子树的组装方式。
额外细节
- 这些操作(创建React 影子节点,在两个React 影子节点之间创建父子关系)是同步且线程安全的操作,它们从 React (JavaScript) 执行到渲染器 (C++),通常在 JavaScript 线程上。
- React 元素树(及其组成React 元素节点)并非无限期存在。它是 React 中由“光纤”具象化的时间表示。每个代表宿主组件的“光纤”都存储一个指向React 影子节点的 C++ 指针,这通过 JSI 实现。在此文档中了解更多关于“光纤”的信息。
- React 影子树是不可变的。为了更新任何React 影子节点,渲染器会创建一个新的React 影子树。然而,渲染器提供了克隆操作,以提高状态更新的性能(有关更多详细信息,请参阅React 状态更新)。
在上面的例子中,渲染阶段的结果如下所示:

当React 影子树完成后,渲染器会触发React 元素树的提交。
阶段 2. 提交

提交阶段包括两个操作:布局计算和树提升。
- 布局计算:此操作计算每个React 影子节点的位置和大小。在 React Native 中,这涉及调用 Yoga 来计算每个React 影子节点的布局。实际计算需要每个React 影子节点的样式,这些样式源自 JavaScript 中的React 元素。它还需要React 影子树根节点的布局约束,这决定了结果节点可以占据的可用空间量。

- 树提升(新树 → 下一个树):此操作将新的React 影子树提升为“下一个要挂载的树”。此提升表明新的React 影子树具有所有可挂载的信息,并代表React 元素树的最新状态。“下一个树”将在 UI 线程的下一个“tick”上挂载。
额外细节
- 这些操作在后台线程上异步执行。
- 大部分布局计算完全在 C++ 中执行。然而,某些组件的布局计算取决于宿主平台(例如,
Text、TextInput等)。文本的大小和位置是每个宿主平台特有的,需要在宿主平台层计算。为此,Yoga 调用宿主平台中定义的一个函数来计算组件的布局。
阶段 3. 挂载

挂载阶段将React 影子树(现在包含布局计算的数据)转换为具有屏幕上渲染像素的宿主视图树。提醒一下,React 元素树看起来像这样:
<View>
<Text>Hello, World</Text>
</View>
从高层来看,React Native 渲染器为每个React 影子节点创建一个相应的宿主视图并将其挂载到屏幕上。在上面的示例中,渲染器为<View>创建一个android.view.ViewGroup实例,为<Text>创建一个android.widget.TextView实例,并用“Hello World”填充它。类似地,对于 iOS,将创建一个UIView并通过调用NSLayoutManager填充文本。然后,每个宿主视图都配置为使用其 React 影子节点中的 props,并使用计算出的布局信息配置其大小和位置。

更详细地说,挂载阶段包括以下三个步骤:
- 树差异计算:此步骤完全在 C++ 中计算“先前渲染的树”和“下一个树”之间的差异。结果是一个要对宿主视图执行的原子突变操作列表(例如,
createView、updateView、removeView、deleteView等)。此步骤也是 React 影子树被扁平化以避免创建不必要的宿主视图的地方。有关此算法的详细信息,请参阅视图扁平化。 - 树提升(下一个树 → 渲染树):此步骤原子地将“下一个树”提升为“先前渲染的树”,以便下一个挂载阶段针对正确的树计算差异。
- 视图挂载:此步骤将原子突变操作应用于相应的宿主视图。此步骤在宿主平台的 UI 线程中执行。
额外细节
- 这些操作在 UI 线程上同步执行。如果提交阶段在后台线程上执行,则挂载阶段会安排在 UI 线程的下一个“tick”执行。另一方面,如果提交阶段在 UI 线程上执行,则挂载阶段会在同一线程上同步执行。
- 挂载阶段的调度、实现和执行在很大程度上取决于宿主平台。例如,挂载层的渲染器架构目前在 Android 和 iOS 之间有所不同。
- 在初始渲染期间,“先前渲染的树”是空的。因此,树差异计算步骤将产生一个仅包含创建视图、设置属性和相互添加视图的突变操作列表。在处理React 状态更新时,树差异计算对于性能变得更加重要。
- 在当前的生产测试中,一个React 影子树通常包含大约 600-1000 个React 影子节点(在视图扁平化之前),在视图扁平化之后,这些树被缩减到约 200 个节点。在 iPad 或桌面应用程序中,这个数量可能会增加 10 倍。
React 状态更新
让我们探讨React 元素树状态更新时渲染管道的每个阶段。假设您在初始渲染中渲染了以下组件:
function MyComponent() {
return (
<View>
<View
style={{backgroundColor: 'red', height: 20, width: 20}}
/>
<View
style={{backgroundColor: 'blue', height: 20, width: 20}}
/>
</View>
);
}
应用初始渲染部分中描述的内容,您应该会创建以下树:

请注意,节点 3 映射到一个红色背景的宿主视图,而节点 4 映射到一个蓝色背景的宿主视图。假设由于 JavaScript 产品逻辑中的状态更新,第一个嵌套 <View> 的背景从 'red' 变为 'yellow'。新的React 元素树可能看起来像这样:
<View>
<View
style={{backgroundColor: 'yellow', height: 20, width: 20}}
/>
<View
style={{backgroundColor: 'blue', height: 20, width: 20}}
/>
</View>
React Native 如何处理此更新?
当发生状态更新时,渲染器需要在概念上更新React 元素树,以更新已挂载的宿主视图。但为了保持线程安全,React 元素树和React 影子树都必须是不可变的。这意味着 React 必须创建每个树的新副本,其中包含新的 props、样式和子元素,而不是修改当前的React 元素树和React 影子树。
让我们探讨状态更新期间渲染管道的每个阶段。
阶段 1. 渲染

当 React 创建一个包含新状态的React 元素树时,它必须克隆受更改影响的每个React 元素和React 影子节点。克隆后,新的React 影子树将被提交。
React Native 渲染器利用结构共享来最大程度地减少不可变性的开销。当克隆一个React 元素以包含新状态时,所有位于通往根节点的路径上的React 元素都会被克隆。只有当一个 React 元素需要更新其 props、样式或子元素时,React 才会克隆它。任何未被状态更新改变的React 元素都会被旧树和新树共享。
在上面的例子中,React 使用以下操作创建新树:
- CloneNode(节点 3,
{backgroundColor: 'yellow'}) → 节点 3' - CloneNode(节点 2) → 节点 2'
- AppendChild(节点 2', 节点 3')
- AppendChild(节点 2', 节点 4)
- CloneNode(节点 1) → 节点 1'
- AppendChild(节点 1', 节点 2')
完成这些操作后,节点 1'代表新的React 元素树的根。我们将T赋值给“先前渲染的树”,将T'赋值给“新树”。

请注意T和T'如何共享节点 4。结构共享提高了性能并减少了内存使用。
阶段 2. 提交

在 React 创建新的React 元素树和React 影子树之后,它必须提交它们。
- 布局计算:类似于初始渲染期间的布局计算。一个重要的区别是布局计算可能会导致共享的React 影子节点被克隆。这可能发生,因为如果共享的React 影子节点的父节点发生布局更改,共享的React 影子节点的布局也可能发生更改。
- 树提升(新树 → 下一个树):类似于初始渲染期间的树提升。
阶段 3. 挂载

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

React Native 渲染器状态更新
对于影子树中的大多数信息,React 是唯一的拥有者和唯一的真理来源。所有数据都源自 React,并且数据流是单向的。
然而,有一个例外和重要的机制:C++ 中的组件可以包含不直接暴露给 JavaScript 的状态,并且 JavaScript 不是真理的来源。C++ 和宿主平台控制此C++ 状态。通常,这仅在您正在开发需要C++ 状态的复杂宿主组件时才相关。绝大多数宿主组件不需要此功能。
例如,ScrollView使用这种机制让渲染器知道当前的偏移量。更新由宿主平台触发,特别是代表ScrollView组件的宿主视图。偏移量信息用于measure等 API 中。由于此更新源自宿主平台,并且不影响 React 元素树,因此此状态数据由C++ 状态持有。
从概念上讲,C++ 状态更新类似于上面描述的React 状态更新。但有两个重要区别:
- 它们跳过“渲染阶段”,因为 React 不参与其中。
- 更新可以在任何线程上发起和发生,包括主线程。
阶段 2. 提交

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

挂载阶段实际上与React 状态更新的挂载阶段相同。渲染器仍然需要重新计算布局,执行树差异计算等。详细信息请参阅上述部分。