关于react-native:React-Native新架构剖析

5次阅读

共计 9729 个字符,预计需要花费 25 分钟才能阅读完成。

目前 React Native 新架构所依赖的 React 18 曾经发了 beta 版,React Native 新架构面向生态库和外围开发者的文档也正式公布,React Native 团队成员 Kevin Gozali 也在最近一次访谈中谈到新架构离正式发版还差最初一步提早初始化,而最初一步工作大概会在 2022 年上半年实现。种种迹象表明,React Native 新架构真的要来了。

后面,RN 官网发表:Hermes 将成为 React Native 默认的 JS 引擎。在文章中,咱们简略的介绍了行将公布的新渲染器 Fabric,那么咱们重点来意识下这个新的渲染器 Fabric。

一、Fabric

1.1 基本概念

Fabric 是 React Native 新架构的渲染零碎,是从老架构的渲染零碎演变而来的。外围原理是在 C++ 层对立更多的渲染逻辑,晋升与宿主平台(host platforms)互操作性,即可能在 UI 线程上同步调用 JavaScript 代码,渲染效率失去显著的进步。Fabric 研发始于 2018 年,Facebook 外部的很多 React Native 利用应用的就是新的渲染器 Fabric。

在简介新渲染器(new renderer)之前,咱们先介绍几个单词和概念:

  • 宿主平台(Host platform):React Native 嵌入的平台,比方 Android、iOS、Windows、macOS。
  • Fabric 渲染器(Fabric Renderer):React Native 执行的 React 框架代码,和 React 在 Web 中执行代码是同一份。然而,React Native 渲染的是通用平台视图(宿主视图)而不是 DOM 节点(能够认为 DOM 是 Web 的宿主视图)。

在更换了底层的渲染流程之后,Fabric 渲染器使得渲染宿主视图变得可行。Fabric 让 React 与各个平台间接通信并治理其宿主视图实例。Fabric 渲染器存在于 JavaScript 中,并且它调用的是由 C++ 代码裸露的接口。

1.2 新渲染器的初衷

开发新的渲染架构的初衷是为了更好的晋升用户体验,而这种新体验是在老架构上是不可能实现的。次要体现为:

  • 晋升宿主视图(host views)和 React 视图(React views)的互操作性,渲染器必须有能力同步地测量和渲染 React 界面。在老架构中,React Native 布局是异步的,这导致在宿主视图中渲染嵌套的 React Native 视图,会有布局“抖动”的问题。
  • 借助多优先级和同步事件的能力,渲染器能够进步用户交互的优先级,来确保他们的操作失去及时的解决。
  • React Suspense 的集成,容许开发者在 React 中更正当的组织申请数据代码。
  • 容许开发者在 React Native 应用 React Concurrent 中断渲染性能。
  • 更容易的实现 React Native 的服务端渲染。

除此之外,新的 Fabric 渲染器在代码品质、性能、可扩展性方面也是有了质的飞升。

  • 类型平安:代码生成工具(code generation)确保了 JS 和宿主平台两方面的类型平安。代码生成工具应用 JavaScript 组件申明作为惟一事实源,生成 C++ 构造体来持有 props 属性。不会因为 JavaScript 和宿主组件 props 属性不匹配而呈现构建谬误。
  • 共享 C++ core:渲染器是用 C++ 实现的,其外围 core 在平台之间是共享的。这减少了一致性并且使得新的平台可能更容易采纳 React Native。(译注:例如 VR 新平台)
  • 更好的宿主平台互操作性:当宿主组件集成到 React Native 时,同步和线程平安的布局计算晋升了用户体验(译注:没有异步的【抖动】)。
  • 性能晋升:新的渲染零碎的实现是跨平台的,每个平台都从那些本来只在某个特定平台的实现的性能优化中失去了更好的用户体验。比方拍平视图层级,本来只是 Android 上的性能优化计划,当初 Android 和 iOS 都间接有了。
  • 一致性:新的渲染零碎的实现是跨平台的,不同平台之间更容易保持一致。
  • 更快的启动速度:默认状况下,宿主组件的初始化是懒执行的。
  • JS 和宿主平台之间的数据序列化更少:React 应用序列化 JSON 在 JavaScript 和宿主平台之间传递数据。新的渲染器用 JSI(JavaScript Interface)间接获取 JavaScript 数据。

二、渲染流程

2.1 渲染流程

React Native 渲染器通过一系列加工解决,将 React 代码渲染到宿主平台。这一系列加工解决就是渲染流水线(pipeline),它的作用是初始化渲染和 UI 状态更新。接下来,咱们重点介绍一下 React Native 渲染流水线,及其在各种场景中的不同之处。

渲染流水线可大抵分为三个阶段:

  • 渲染:在 JavaScript 中,React 执行那些产品逻辑代码创立 React 元素树(React Element Trees)。而后在 C++ 中,用 React 元素树创立 React 影子树(React Shadow Tree)。
  • 提交:在 React 影子树齐全创立后,渲染器会触发一次提交。这会将 React 元素树和新创建的 React 影子树的晋升为“下一棵要挂载的树”。这个过程中也包含了布局信息计算。
  • 挂载:React 影子树有了布局计算结果后,它会被转化为一个宿主视图树(Host View Tree)。

这里有几个名词须要解释下:

React 元素树

React 元素树是通过 JavaScript 中的 React 创立的,该树由一系类 React 元素组成。一个 React 元素就是一个一般的 JavaScript 对象,它形容了须要在屏幕中展现的内容。一个元素包含属性 props、款式 styles、子元素 children。React 元素分为两类:React 复合组件实例(React Composite Components)和 React 宿主组件(React Host Components)实例,并且它只存在于 JavaScript 中。

React 影子树

React 影子树是通过 Fabric 渲染器创立的,树由一系列 React 影子节点组成。一个 React 影子节点是一个对象,代表一个曾经挂载的 React 宿主组件,其蕴含的属性 props 来自 JavaScript。它也包含布局信息,比方坐标系 x、y,宽高 width、height。在新渲染器 Fabric 中,React 影子节点对象只存在于 C++ 中。而在老架构中,它存在于手机运行时的堆栈中,比方 Android 的 JVM。

宿主视图树

宿主视图树就是一系列的宿主视图,宿主平台有 Android 平台、iOS 平台等等。在 Android 上,宿主视图就是 android.view.ViewGroup 实例、android.widget.TextView 实例等等。宿主视图就像积木一样地形成了宿主视图树。每个宿主视图的大小和坐标地位基于的是 LayoutMetrics,而 LayoutMetrics 是通过 React Native 得布局引擎 Yoga 计算出来的。宿主视图的款式和内容信息,是从 React 影子树中失去的。

React Native 渲染流水线的各个阶段可能产生在不同的线程中,参考线程模型局部。

在 React Native 中,波及渲染的操作通常有三种:

  • 初始化渲染
  • React 状态更新
  • React Native 渲染器的状态更新

2.2 初始化渲染

2.2.1 渲染阶段

如果,有上面一个组件须要执行渲染:

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

在下面的例子中,<MyComponent />最终会被 React 简化为最根底的 React 宿主元素。每一次递归地调用函数组件 MyComponet,或类组件的 render 办法,直至所有的组件都被调用过。最终,失去一棵 React 宿主组件的 React 元素树。

在这里,有几个重要的名词须要解释下“

  • React 组件:React 组件就是 JavaScript 函数或者类,形容如何创立 React 元素。
  • React 复合组件:React 组件的 render 办法中,包含其余 React 复合组件和 React 宿主组件。(译注:复合组件就是开发者申明的组件)
  • React 宿主组件 :React 组件的视图是通过宿主视图,比方 <View><Text> 实现的。在 Web 中,ReactDOM 的宿主组件就是 <p>标签、<div>标签代表的组件。

在元素简化的过程中,每调用一个 React 元素,渲染器同时会同步地创立 React 影子节点。这个过程只产生在 React 宿主组件上,不会产生在 React 复合组件上。比方,一个 <View>会创立一个 ViewShadowNode 对象,一个 <Text> 会创立一个 TextShadowNode 对象。而咱们开发的组件,因为不是根底组件,因而没有间接的 React 影子节点与之对应,所以 <MyComponent> 并没有间接对应的 React 影子节点。

在 React 为两个 React 元素节点创立一对父子关系的同时,渲染器也会为对应的 React 影子节点创立一样的父子关系。下面代码,各个渲染阶段的产物如下图所示。

2.2.2 提交阶段

在 React 影子树创立实现后,渲染器触发了一次 React 元素树的提交。

提交阶段(Commit Phase)由两个操作组成:布局计算和树晋升。

布局计算

这一步会计算每个 React 影子节点的地位和大小。在 React Native 中,每一个 React 影子节点的布局都是通过 Yoga 布局引擎来计算的。理论的计算须要思考每一个 React 影子节点的款式,该款式来自于 JavaScript 中的 React 元素。计算还须要思考 React 影子树的根节点的布局束缚,这决定了最终节点可能领有多少可用空间。

树晋升

从新树到下一棵树(Tree Promotion,New Tree → Next Tree),这一步会将新的 React 影子树晋升为要挂载的下一棵树。这次晋升代表着新树领有了所有要挂载的信息,并且可能代表 React 元素树的最新状态,下一棵树会在 UI 线程下一个“tick”进行挂载(译注:tick 是 CUP 的最小工夫单元)。

并且,绝大多数布局计算都是 C++ 中执行,只有某些组件,比方 Text、TextInput 组件等的布局计算是在宿主平台执行的。文字的大小和地位在每个宿主平台都是特地的,须要在宿主平台层进行计算。为此,Yoga 布局引擎调用了宿主平台的函数来计算这些组件的布局。

2.2.3 挂载阶段


挂载阶段(Mount Phase)会将曾经蕴含布局计算数据的 React 影子树,转换为以像素模式渲染在屏幕中的宿主视图树。

站在更高的抽象层次上,React Native 渲染器为每个 React 影子节点创立了对应的宿主视图,并且将它们挂载在屏幕上。在下面的例子中,渲染器为<View> 创立了 android.view.ViewGroup 实例,为 <Text> 创立了文字内容为“Hello World”的 android.widget.TextView 实例。iOS 也是相似的,创立了一个 UIView 并调用 NSLayoutManager 创立文本。而后会为宿主视图配置来自 React 影子节点上的属性,这些宿主视图的大小地位都是通过计算好的布局信息配置的。


挂载阶段又细分为三个步骤:

  • 树比照:这个步骤齐全用的是 C++ 计算的,会比照“曾经渲染的树”和”下一棵树”之间的元素差别。计算的后果是一系列宿主平台上的原子变更操作,比方 createView, updateView, removeView, deleteView 等等。在这个步骤中,还会将 React 影子树重构,来防止不必要的宿主视图创立。
  • 树晋升,从下一棵树到已渲染树:在这个步骤中,会主动将“下一棵树”晋升为“先前渲染的树”,因而在下一个挂载阶段,树的比照计算用的是正确的树。
  • 视图挂载:这个步骤会在对应的原生视图上执行原子变更操作,该步骤是产生在原生平台的 UI 线程的。

同时,挂载阶段的所有操作都是在 UI 线程同步执行的。如果提交阶段是在后盾线程执行,那么在挂载阶段会在 UI 线程的下一个“tick”执行。另外,如果提交阶段是在 UI 线程执行的,那么挂载阶段也是在 UI 线程执行。挂载阶段的调度和执行很大水平取决于宿主平台。例如,以后 Android 和 iOS 挂载层的渲染架构是不一样的。

2.3 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> 的背景色彩由红色改为黄色。新的 React 元素树看起来大略是这样的。

<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 影子树,而是必须为每棵树创立一个蕴含新属性、新款式和新子节点的新正本。

2.3.1 渲染阶段


React 要创立了一个蕴含新状态的新的 React 元素树,它就要复制所有变更的 React 元素和 React 影子节点。复制后,再提交新的 React 元素树。

React Native 渲染器利用构造共享的形式,将不可变个性的开销变得最小。为了更新 React 元素的新状态,从该元素到根元素门路上的所有元素都须要复制。但 React 只会复制有新属性、新款式或新子元素的 React 元素,任何没有因状态更新产生变动的 React 元素都不会复制,而是由新树和旧树共享。

在下面的例子中,React 创立新树应用了上面这些操作:

  1. CloneNode(Node 3, {backgroundColor: ‘yellow’}) → Node 3′
  2. CloneNode(Node 2) → Node 2′
  3. AppendChild(Node 2′, Node 3′)
  4. AppendChild(Node 2′, Node 4)
  5. CloneNode(Node 1) → Node 1′
  6. AppendChild(Node 1′, Node 2′)

操作实现后,节点 1’(Node 1’)就是新的 React 元素树的根节点,咱们用 T 代表“先前渲染的树”,用 T’ 代表“新树”。

留神,节点 4 在 T and T’ 之间是共享的。构造共享晋升了性能并缩小了内存的应用。

2.3.2 提交阶段


在 React 创立完新的 React 元素树和 React 影子树后,须要提交它们,也会波及以下几个步骤:

  • 布局计算:状态更新时的布局计算,和初始化渲染的布局计算相似。一个重要的不同之处是布局计算可能会导致共享的 React 影子节点被复制。这是因为,如果共享的 React 影子节点的父节点引起了布局扭转,共享的 React 影子节点的布局也可能产生扭转。
  • 树晋升: 和初始化渲染的树晋升相似。
  • 树比照:这个步骤会计算“先前渲染的树”(T)和“下一棵树”(T’)的区别。计算的后果是原生视图的变更操作。

在下面的例子中,这些操作包含:UpdateView(‘Node 3’, {backgroundColor: ‘yellow’})

2.3.3 挂载阶段

  • 树晋升:在这个步骤中,会主动将“下一棵树”晋升为“先前渲染的树”,因而在下一个挂载阶段,树的比照计算用的是正确的树。
  • 视图挂载:这个步骤会在对应的原生视图上执行原子变更操作。在下面的例子中,只有 视图 3(View 3)的背景色彩会更新,变为黄色。

2.4 渲染器状态更新

对于影子树中的大多数信息而言,React 是惟一所有方也是惟一事实源。并且所有来源于 React 的数据都是单向流动的。

但有一个例外。这个例外是一种十分重要的机制:C++ 组件能够领有状态,且该状态能够不间接裸露给 JavaScript,这时候 JavaScript(或 React)就不是惟一事实源了。通常,只有简单的宿主组件才会用到 C++ 状态,绝大多数宿主组件都不须要此性能。

例如,ScrollView 应用这种机制让渲染器晓得以后的偏移量是多少。偏移量的更新是宿主平台的触发,具体地说是 ScrollView 组件。这些偏移量信息在 React Native 的 measure 等 API 中有用到。因为偏移量数据是由 C++ 状态持有的,所以源于宿主平台更新,不影响 React 元素树。

从概念上讲,C++ 状态更新相似于咱们后面提到的 React 状态更新,但有两点不同:

  • 因为不波及 React,所以跳过了“渲染阶段”(Render phase)。
  • 更新能够源自和产生在任何线程,包含主线程。


提交阶段(Commit Phase):在执行 C++ 状态更新时,会有一段代码把影子节点(N)的 C++ 状态设置为值 S。React Native 渲染器会重复尝试获取 N 的最新提交版本,并应用新状态 S 复制它,并将新的影子节点 N’ 提交给影子树。如果 React 在此期间执行了另一次提交,或者其余 C++ 状态有了更新,本次 C++ 状态提交失败。这时渲染器将多次重试 C++ 状态更新,直到提交胜利,这能够避免实在源的抵触和竞争。


挂载阶段(Mount Phase)实际上与 React 状态更新的挂载阶段雷同。渲染器依然须要从新计算布局、执行树比照等操作。

三、跨平台实现

在上一代 React Native 渲染器中,React 影子树、布局逻辑、视图拍平算法是在各个平台独自实现的。以后的渲染器的设计上采纳的是跨平台的解决方案,共享了外围的 C++ 实现。而 Fabric 渲染器间接应用 C++ core 渲染实现了跨平台共享。

应用 C++ 作为外围渲染零碎有以下几个长处。

  • 繁多实现升高了开发和保护老本。
  • 晋升了创立 React 影子树的性能,同时在 Android 上,也因为不再应用 JNI for Yoga,升高了 Yoga 渲染引擎的开销,布局计算的性能也有所晋升。
  • 每个 React 影子节点在 C++ 中占用的内存,比在 Kotlin 或 Swift 中占用的要小。

同时,React Native 团队还应用了强制不可变的 C++ 个性,来确保并发拜访时共享资源即使不加锁爱护,也不会有问题。但在 Android 端还有两种例外,渲染器仍然会有 JNI 的开销:

  • 简单视图,比方 Text、TextInput 等,仍然会应用 JNI 来传输属性 props。
  • 在挂载阶段仍然会应用 JNI 来发送变更操作。

React Native 团队在摸索应用 ByteBuffer 序列化数据这种新的机制,来替换 ReadableMap,缩小 JNI 的开销,指标是将 JNI 的开销缩小 35~50%。

渲染器提供了 C++ 与两边通信的 API:

  • 与 React 通信
  • 与宿主平台通信

对于 React 与渲染器的通信,包含 渲染(render)React 树和监听 事件(event),比方 onLayout、onKeyPress、touch 等。而 React Native 渲染器与宿主平台的通信,包含在屏幕上 挂载(mount)宿主视图,包含 create、insert、update、delete 宿主视图,和监听用户在宿主平台产生的 事件(event)。

四、视图拍平

视图拍平(View Flattening)是 React Native 渲染器防止布局嵌套太深的优化伎俩。React API 在设计上心愿通过组合的形式,实现组件申明和重用,这为更简略的开发提供了一个很好的模型。然而在实现中,API 的这些个性会导致一些 React 元素会嵌套地很深,而其中大部分 React 元素节点只会影响视图布局,并不会在屏幕中渲染任何内容。这就是所谓的“只参加布局”类型节点。

从概念上讲,React 元素树的节点数量和屏幕上的视图数量应该是 1:1 的关系。然而,渲染一个很深的“只参加布局”的 React 元素会导致性能变慢。如果,有一个利用,利用中领有外边距 ContainerComponent 的容器组件,容器组件的子组件是 TitleComponent 题目组件,题目组件包含一个图片和一行文字。React 代码示例如下:

function MyComponent() {
  return (
    <View>                          // ReactAppComponent
      <View style={{margin: 10}} /> // ContainerComponent
        <View style={{margin: 10}}> // TitleComponent
          <Image {...} />
          <Text {...}>This is a title</Text>
        </View>
      </View>
    </View>
  );
}

React Native 在渲染时,会生成以下三棵树:

在视图 2 和视图 3 是“只参加布局”的视图,因为它们在屏幕上渲染只是为了提供 10 像素的外边距。

为了晋升 React 元素树中“只参加布局”类型的性能,渲染器实现了一种视图拍平的机制来合并或拍平这类节点,缩小屏幕中宿主视图的层级深度。该算法思考到了如下属性,比方 margin、padding、backgroundColor 和 opacity 等等。

视图拍平算法是渲染器的比照(diffing)阶段的一部分,这样设计的益处是咱们不须要额定的 CUP 耗时,来拍平 React 元素树中“只参加布局”的视图。此外,作为 C++ 外围的一部分,视图拍平算法默认是全平台共用的。

在后面的例子中,视图 2 和视图 3 会作为“比照算法”(diffing algorithm)的一部分被拍平,而它们的款式后果会被合并到视图 1 中。


不过,尽管这种优化让渲染器少创立和渲染两个宿主视图,但从用户的角度看屏幕内容没有任何区别。

五、线程模型

React Native 渲染器是线程平安的。从更高的视角看,在框架外部线程平安是通过不可变的数据后果保障的,其应用的是 C++ 的 const correctness 个性。这意味着,在渲染器中 React 的每次更新都会从新创立或复制新对象,而不是更新原有的数据结构。这是框架把线程平安和同步 API 裸露给 React 的前提。

在 React Native 中,渲染器应用三个不同的线程:

  • UI 线程:惟一能够操作宿主视图的线程。
  • JavaScript 线程:这是执行 React 渲染阶段的中央。
  • 后盾线程:专门用于布局的线程。

下图形容了 React Native 渲染的残缺流程:

5.1 渲染场景

在后盾线程中渲染

这是最常见的场景,大多数的渲染流水线产生在 JavaScript 线程和后盾线程。

在主线程中渲染

当 UI 线程上有高优先级事件时,渲染器可能在 UI 线程上同步执行所有渲染流水线。

默认或间断事件中断

在这个场景中,UI 线程的低优先级事件中断了渲染步骤。React 和 React Native 渲染器可能中断渲染步骤,并把它的状态和一个在 UI 线程执行的低优先级事件合并。在这个例子中渲染过程会持续在后盾线程中执行。

不相干的事件中断

渲染步骤是可中断的。在这个场景中,UI 线程的高优先级事件中断了渲染步骤。React 和渲染器是可能打断渲染步骤的,并把它的状态和 UI 线程执行的高优先级事件合并。在 UI 线程渲染步骤是同步执行的。

来自 JavaScript 线程的后盾线程批量更新

在后盾线程将更新分派给 UI 线程之前,它会查看是否有新的更新来自 JavaScript。这样,当渲染器晓得新的状态要到来时,它就不会间接渲染旧的状态。

C++ 状态更新

更新来自 UI 线程,并会跳过渲染步骤。

正文完
 0