跳至主内容

渲染、提交与挂载

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

注意

本文涉及正在逐步推出的新架构

React Native 渲染器通过一系列工作将 React 逻辑渲染到宿主平台。这一系列工作称为渲染管线,发生在初始渲染和 UI 状态更新时。本文将介绍渲染管线及其在不同场景下的差异。

渲染管线可分为三个主要阶段:

  1. 渲染: React 执行业务逻辑,在 JavaScript 中创建 React 元素树。基于此树,渲染器在 C++ 中创建 React 影子树

  2. 提交: 当 React 影子树完全创建后,渲染器触发提交操作。该操作将 React 元素树和新创建的 React 影子树提升为待挂载的"下一棵树",同时调度布局计算任务。

  3. 挂载: 完成布局计算的 React 影子树被转换为宿主视图树

渲染管线的各阶段可能在不同线程执行。详见线程模型文档。

React Native 渲染器数据流


初始渲染

假设需要渲染以下内容:

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

// <MyComponent />

上例中 <MyComponent />React 元素。React 通过递归调用该元素(若使用 JavaScript 类实现则调用其 render 方法),将其逐步化简为最终的 React 宿主组件,直至所有 React 元素无法再化简。此时便得到了由 React 宿主组件构成的 React 元素树。

阶段1. 渲染

阶段一:渲染

在元素化简过程中,每当调用一个 React 元素,渲染器会同步创建对应的 React 影子节点。此操作仅针对 React 宿主组件,不适用于 React 复合组件。上例中,<View> 对应创建 ViewShadowNode 对象,<Text> 对应创建 TextShadowNode 对象。需特别注意:不存在直接代表 <MyComponent>React 影子节点

当 React 在两个 React 元素节点间建立父子关系时,渲染器会在对应的 React 影子节点间建立相同关系。React 影子树正是通过这种方式组装而成。

补充说明

  • 创建 React 影子节点、建立 React 影子节点间父子关系等操作是同步且线程安全的,通常在主线程(JavaScript 线程)执行,从 React(JavaScript)传递到渲染器(C++)。

  • React 元素树(及其组成的 React 元素节点)不会永久存在,它是 React 中由"fiber"实现的临时表示形式。每个代表宿主组件的"fiber"通过 JSI 存储指向 React 影子节点的 C++ 指针。在此文档详细了解"fiber"

  • React Shadow Tree 是不可变的。要更新任何 React Shadow Node,渲染器会创建新的 React Shadow Tree。不过,渲染器提供了克隆操作来优化状态更新的性能(详见 React 状态更新)。

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

步骤一

React Shadow Tree 构建完成后,渲染器会触发对 React Element Tree 的提交。

阶段 2:提交

阶段二:提交

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

  • 布局计算(Layout Calculation):该操作会计算每个 React Shadow Node 的位置和尺寸。在 React Native 中,这涉及到调用 Yoga 来计算每个 React Shadow Node 的布局。实际计算需要每个 React Shadow Node 的样式(这些样式来源于 JavaScript 中的 React Element),同时还需要 React Shadow Tree 根节点的布局约束,这决定了最终节点可占用的可用空间大小。

步骤二

  • 树提升(Tree Promotion)(新树 → 下一棵树):该操作将新的 React Shadow Tree 提升为待挂载的“下一棵树”。这一提升意味着新的 React Shadow Tree 已具备挂载所需的全部信息,并代表了 React Element Tree 的最新状态。“下一棵树”将在 UI 线程的下一个时间点(tick)进行挂载。

补充说明

  • 这些操作在后台线程上异步执行。

  • 大部分布局计算完全在 C++ 中执行。然而,某些组件(例如 TextTextInput 等)的布局计算依赖于 宿主平台(host platform)。文本的尺寸和位置因宿主平台而异,因此需要在宿主平台层进行计算。为此,Yoga 会调用宿主平台中定义的函数来计算组件的布局。

阶段 3:挂载

阶段三:挂载

挂载阶段将 React Shadow Tree(此时已包含布局计算的数据)转换为屏幕上带有渲染像素的 宿主视图树(Host View Tree)。回顾一下,React Element Tree 的结构如下:

jsx
<View>
<Text>Hello, World</Text>
</View>

概括来说,React Native 渲染器会为每个 React Shadow Node 创建一个对应的 宿主视图(Host View) 并将其挂载到屏幕上。在以上示例中,渲染器为 <View> 创建一个 android.view.ViewGroup 实例,为 <Text> 创建一个 android.widget.TextView 实例,并在其中填充“Hello World”文本。类似地,在 iOS 上会创建一个 UIView,并通过调用 NSLayoutManager 来填充文本。然后,每个宿主视图会使用其 React Shadow Node 中的属性(props)进行配置,并使用计算得出的布局信息来设置其尺寸和位置。

步骤二

更详细地说,挂载阶段包含以下三个步骤:

  • 树差异计算(Tree Diffing):该步骤完全在 C++ 中计算“先前渲染的树”与“下一棵树”之间的差异。结果会生成一个要在宿主视图上执行的原子变更操作列表(例如 createViewupdateViewremoveViewdeleteView 等)。此步骤还会将 React Shadow Tree 扁平化,以避免创建不必要的宿主视图。有关此算法的详细信息,请参阅 视图扁平化

  • 树升级(待挂载树 → 已渲染树):此步骤将"待挂载树"原子性地升级为"先前渲染树",确保后续挂载阶段能与正确的基准树进行差异计算。

  • 视图挂载:此步骤将原子化变更操作应用到对应的宿主视图上,在UI线程的宿主平台中执行。

补充说明

  • 操作在UI线程同步执行。若提交阶段在后台线程执行,挂载操作会调度到UI线程的下个"tick";若提交阶段已在UI线程,则挂载同步执行。

  • 挂载阶段的调度、实现和执行高度依赖宿主平台。例如Android和iOS的挂载层渲染架构当前存在差异。

  • 初始渲染时"previously rendered tree"为空,因此树差异计算生成的变更操作列表仅包含创建视图、设置属性和视图添加操作。在处理React状态更新时,树差异计算对性能优化更为关键。

  • 当前生产环境测试显示:React影子树通常包含600-1000个节点(视图扁平化前),扁平化后缩减至约200节点。iPad或桌面应用中该数量可能增长10倍。


React状态更新

接下来探索React元素树状态更新时渲染管线的各阶段。假设初始渲染后组件状态变更:

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

根据初始渲染章节描述,您将期望创建以下树结构:

渲染管线4

注意节点3对应红色背景的宿主视图,节点4对应蓝色背景的宿主视图。当JavaScript业务逻辑状态更新导致第一个嵌套<View>的背景色从'red'变为'yellow'时,新的_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需创建包含新props、样式和子元素的全新树副本,而非直接修改现有树。

下面解析状态更新时渲染管线的各阶段。

阶段1. 渲染

阶段一:渲染

当React创建包含新状态的新React元素树时,必须克隆所有受变更影响的React元素和影子节点。克隆完成后,新的React影子树进入提交阶段。

React Native渲染器采用结构共享最小化不可变性的开销。克隆React元素时,仅会克隆从变更点到根节点路径上的元素。只有当React元素的props、样式或子元素需要更新时才会被克隆。未受状态更新影响的元素在新旧树间共享。

上例中,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' 定义为"新树":

渲染管线 5

注意 TT' 都共享节点 4。这种结构共享机制提升了性能并减少了内存占用。

阶段 2:提交

阶段二:提交

当 React 创建完新的 _React 元素树_和 _React 影子树_后,必须提交它们。

  • 布局计算:与初始渲染中的布局计算类似。重要区别在于布局计算可能导致共享的 _React 影子节点_被克隆。这是因为如果父节点的布局发生变化,共享的子节点布局也可能随之改变。

  • 树升级(新树 → 待挂载树):与初始渲染中的树升级机制相同。

阶段 3:挂载

阶段三:挂载

  • 树升级(待挂载树 → 已渲染树):此步骤将"待挂载树"原子性地升级为"先前渲染树",确保后续挂载阶段能与正确的基准树进行差异计算。

  • 树差异计算:计算"先前渲染树"(T)与"待挂载树"(T')之间的差异。结果生成针对 _宿主视图_的原子化变更操作序列:

    • 上例中的操作是:UpdateView(**Node 3**, {backgroundColor: 'yellow'})
    • 差异计算可在任意当前已挂载树与任意新树之间进行,渲染器可跳过某些中间树版本。
  • 视图挂载:将原子化变更操作应用到对应的 宿主视图。上例中仅视图 3backgroundColor 会被更新(变为黄色)。

渲染管线 6


React Native 渲染器状态更新

对于 _影子树_中的大多数信息,React 是唯一所有者和单一数据源。所有数据都源自 React 并遵循单向数据流。

但存在一个例外的重要机制:C++ 中的组件可包含不直接暴露给 JavaScript 的状态,且 JavaScript 不是其数据源。这类 _C++ 状态_由 C++ 和 _宿主平台_控制。通常仅当开发需要 _C++ 状态_的复杂 _宿主组件_时才涉及此机制,绝大多数 _宿主组件_无需此功能。

例如:ScrollView 使用此机制向渲染器通知当前滚动偏移量。更新由 宿主平台(特别是代表 ScrollView 组件的宿主视图)触发。该偏移量信息用于 measure 等 API。由于这类更新源自宿主平台且不影响 React 元素树,因此状态数据由 _C++ 状态_维护。

概念上,_C++ 状态_更新与上文描述的 React 状态更新 相似,但有两点重要区别:

  1. 它们跳过“渲染阶段”,因为 React 不参与其中。

  2. 更新可以源自任何线程并发生在任何线程上,包括主线程。

阶段 2:提交

阶段二:提交

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

阶段 3:挂载

阶段三:挂载

_挂载阶段_实际上与 React 状态更新的挂载阶段 相同。渲染器仍然需要重新计算布局、执行树差异比较等。详情请参阅上文。