概述
React Native 渲染器通过一系列加工处理,将 React 代码渲染到宿主平台。这一系列加工处理就是渲染流水线(pipeline),它的作用是初始化渲染和 UI 状态更新。 接下来介绍的是渲染流水线,及其在各种场景中的不同之处。
渲染(Render)
在 JavaScript 中,React 执行那些产品逻辑代码创建 React 元素树(React Element Trees)。然后在 C++ 中,用 React 元素树创建 React 影子树(React Shadow Tree)。
提交(Commit)
在 React 影子树完全创建后,渲染器会触发一次提交。这会将 React 元素树和新创建的 React 影子树的提升为“下一棵要挂载的树”。 这个过程中也包括了布局信息(在c++中)计算,在更新时会有dom diff计算。
挂载(Mount)
React 影子树有了布局计算结果后,它会被转化为一个宿主视图树(Host View Tree)。
初始化渲染
渲染阶段
想象一下你准备渲染一个组件:
function MyComponent() {
return (
<View>
<Text>Hello, World</Text>
</View>
);
}
// <MyComponent />
在上面的例子中,
名词解释: React 组件(React Component):React 组件就是 JavaScript 函数或者类,描述如何创建 React 元素。
React 复合组件(React Composite Components):复合组件就是开发者声明的组件,上边的MyComponent。
React 宿主组件(React Host Components):React 组件的视图是通过宿主视图,比如
、 ,实现的。在 Web 中,ReactDOM 的宿主组件就是 标签、
标签代表的组件。
在元素简化的过程中,每调用一个 React 元素,渲染器同时会同步地创建 React 影子节点。这个过程只发生在 React 宿主组件上,不会发生在 React 复合组件上。比如,一个
提交阶段
在 React 影子树创建完成后,渲染器触发了一次 React 元素树的提交。
提交阶段(Commit Phase)由两个操作组成:布局计算和树的提升。
布局计算(Layout Calculation)
- 这一步会计算每个 React 影子节点的位置和大小。在 React Native 中,每一个 React 影子节点的布局都是通过 Yoga 布局引擎来计算的。实际的计算需要考虑每一个 React 影子节点的样式,该样式来自于 JavaScript 中的 React 元素。计算还需要考虑 React 影子树的根节点的布局约束,这决定了最终节点能够拥有多少可用空间。
树提升,从新树到下一棵树(Tree Promotion,New Tree → Next Tree)
- 这一步会将新的 React 影子树提升为要挂载的下一棵树。这次提升代表着新树拥有了所有要挂载的信息,并且能够代表 React 元素树的最新状态。下一棵树会在 UI 线程下一个“tick”进行挂载。(译注:tick 是 CPU 的最小时间单元)
更多细节
- 这些操作都是在后台线程中异步执行的。
- 绝大多数布局计算都是 C++ 中执行,只有某些组件,比如 Text、TextInput 组件等等,的布局计算是在宿主平台执行的。文字的大小和位置在每个宿主平台都是特别的,需要在宿主平台层进行计算。为此,Yoga 布局引擎调用了宿主平台的函数来计算这些组件的布局。
挂载阶段
挂载阶段(Mount Phase)会将已经包含布局计算数据的 React 影子树,转换为以像素形式渲染在屏幕中的宿主视图树。请记住,这棵 React 元素树看起来是这样的:
<View>
<Text>Hello, World</Text>
</View>
站在更高的抽象层次上,React Native 渲染器为每个 React 影子节点创建了对应的宿主视图,并且将它们挂载在屏幕上。在上面的例子中,渲染器为
更详细地说,初始化渲染的挂载阶段由三个步骤组成:
树对比(Tree Diffing)
这个步骤完全用的是 C++ 计算的,会对比“已经渲染的树”(previously rendered tree)和”下一棵树”之间的差异。计算的结果是一系列宿主平台上的原子变更操作,比如 createView, updateView, removeView, deleteView 等等。在这个步骤中,还会将 React 影子树拍平,来避免不必要的宿主视图创建。关于视图拍平的算法细节可以在后文找到。
树提升
树提升,从下一棵树到已渲染树(Tree Promotion,Next Tree → Rendered Tree):在这个步骤中,会自动将“下一棵树”提升为“先前渲染的树”,因此在下一个挂载阶段,树的对比计算用的是正确的树。
视图挂载(View Mounting)
这个步骤会在对应的原生视图上执行原子变更操作,该步骤是发生在原生平台的 UI 线程的。
更多细节
- 挂载阶段的所有操作都是在 UI 线程同步执行的。如果提交阶段是在后台线程执行,那么在挂载阶段会在 UI 线程的下一个“tick”执行。另外,如果提交阶段是在 UI 线程执行的,那么挂载阶段也是在 UI 线程执行。
- 挂载阶段的调度和执行很大程度取决于宿主平台。例如,当前 Android 和 iOS 挂载层的渲染架构是不一样的。
- 在初始化渲染时,“先前渲染的树”是空的。因此,树对比(tree diffing)步骤只会生成一系列仅包含创建视图、设置属性、添加视图的变更操作。而在接下来的 React 状态更新场景中,树对比的性能至关重要。
- 在当前生产环境的测试中,在视图拍平之前,React 影子树通常由大约 600-1000 个 React 影子节点组成。在视图拍平之后,树的节点数量会减少到大约 200 个。在 iPad 或桌面应用程序上,这个节点数量可能要乘个 10。
React 状态更新
接下来,我们继续看 React 状态更新时,渲染流水线(render pipeline)的各个阶段是什么样的。假设你在初始化渲染时,渲染的是如下组件:
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>
<View
style={{ backgroundColor: 'yellow', height: 20, width: 20 }}
/>
<View
style={{ backgroundColor: 'blue', height: 20, width: 20 }}
/>
</View>
React Native 是如何处理这个更新的?
从概念上讲,当发生状态更新时,为了更新已经挂载的宿主视图,渲染器需要直接更新 React 元素树。 但是为了线程的安全,React 元素树和 React 影子树都必须是不可变的(immutable)。这意味着 React 并不能直接改变当前的 React 元素树和 React 影子树,而是必须为每棵树创建一个包含新属性、新样式和新子节点的新副本。
让我们继续探究状态更新时,渲染流水线的各个阶段发生了什么。
渲染阶段
React 要创建了一个包含新状态的新的 React 元素树,它就是要复制所有变更的 React 元素和 React 影子节点。 复制后,再提交新的 React 元素树。
React Native 渲染器利用结构共享的方式,将不可变特性的开销变得最小。为了更新 React 元素的新状态,从该元素到根元素路径上的所有元素都需要复制。 但 React 只会复制有新属性、新样式或新子元素的 React 元素,任何没有因状态更新发生变动的 React 元素都不会复制,而是由新树和旧树共享。
在上面的例子中,React 创建新树使用了这些操作:
- CloneNode(Node 3, {backgroundColor: ‘yellow’}) → Node 3’
- CloneNode(Node 2) → Node 2’
- AppendChild(Node 2’, Node 3’)
- AppendChild(Node 2’, Node 4)
- CloneNode(Node 1) → Node 1’
- AppendChild(Node 1’, Node 2’)
操作完成后,节点 1’(Node 1’)就是新的 React 元素树的根节点。我们用 T 代表“先前渲染的树”,用 T’ 代表“新树”。
注意节点 4 在 T and T’ 之间是共享的。结构共享提升了性能并减少了内存的使用。
提交阶段
在 React 创建完新的 React 元素树和 React 影子树后,需要提交它们。
布局计算(Layout Calculation)
状态更新时的布局计算,和初始化渲染的布局计算类似。一个重要的不同之处是布局计算可能会导致共享的 React 影子节点被复制。这是因为,如果共享的 React 影子节点的父节点引起了布局改变,共享的 React 影子节点的布局也可能发生改变。
树提升(Tree Promotion ,New Tree → Next Tree)
和初始化渲染的树提升类似。这一步会将新的 React 影子树提升为要挂载的下一棵树。这次提升代表着新树拥有了所有要挂载的信息,并且能够代表 React 元素树的最新状态。下一棵树会在 UI 线程下一个“tick”进行挂载。(译注:tick 是 CPU 的最小时间单元)
树对比(Tree Diffing)
这个步骤会计算“先前渲染的树”(T)和“下一棵树”(T’)的区别。计算的结果是原生视图的变更操作。
在这个步骤中,会自动将“下一棵树”提升为“先前渲染的树”,因此在下一个挂载阶段,树的对比计算用的是正确的树。
视图挂载(View Mounting)
这个步骤会在对应的原生视图上执行原子变更操作。在上面的例子中,只有视图3(View 3)的背景颜色会更新,变为黄色。
总结
初始化渲染与更新渲染的不同
在初始化渲染时,“先前渲染的树”是空的。因此,树对比(tree diffing)步骤只会生成一系列仅包含创建视图、设置属性、添加视图的变更操作。
初始化渲染时diff发生在挂载mount阶段(因为没什么需要对比的)
更新时发生在提交commit阶段