React Native 新架構是怎麼工作的?

穿越過來的鍵盤手發表於2021-12-26

原文地址:https://reactnative.dev/docs/...

譯者前言:

目前新架構所依賴的 React 18 已經發了 beta 版,React Native 新架構面向生態庫和核心開發者的文件也正式釋出,React Native 團隊成員 Kevin Gozali 也在最近一次訪談中談到新架構離正式發版還差最後一步延遲初始化,而最後一步大約會在 2022 年上半年完成。種種跡象表明,React Native 新架構真的要來了。

後續也會通過極客時間專欄的形式和大家介紹新架構的使用方法、剖析架構原理、講解實踐方案。

由於時間倉促,如果有翻譯不當之處還請大家指出,以下是正文部分。


本文件還在更新持續中,會從概念上介紹 React Native 新架構是如何工作的。目標讀者包括生態庫的開發者、核心貢獻者和特別有好奇心的人。

文件介紹了即將釋出的新渲染器 Fabric 的架構。

Fabric

Fabric 是 React Native 新架構的渲染系統,是從老架構的渲染系統演變而來的。核心原理是在 C++ 層統一更多的渲染邏輯,提升與宿主平臺(host platforms)互操作性,併為 React Native 解鎖更多能力。研發始於 2018 年和 2021年,Facebook 應用中的 React Native 用的就是新的渲染器。

該文件簡介了新渲染器(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++ 程式碼暴露的介面。在這篇文章中有更多關於 React 渲染器的資訊。

新渲染器的初衷和收益

開發新的渲染架構的初衷是為了更好的使用者體驗,而這種新體驗是在老架構上是不可能實現的。比如:

  • 為了提升宿主檢視(host views)和 React 檢視(React views)的互操作性,渲染器必須有能力同步地測量和渲染 React 介面。在老架構中,React Native 佈局是非同步的,這導致在宿主檢視中渲染巢狀的 React Native 檢視,會有佈局“抖動”的問題。
  • 藉助多優先順序和同步事件的能力,渲染器可以提高使用者互動的優先順序,來確保他們的操作得到及時的處理。
  • React Suspense 的整合,允許你在 React 中更符合直覺地寫請求資料程式碼。
  • 允許你在 React Native 使用 React Concurrent 可中斷渲染功能。
  • 更容易實現 React Native 的服務端渲染。

新架構的收益還包括,程式碼質量、效能、可擴充套件性。

  • 型別安全:程式碼生成工具(code generation)確保了 JS 和宿主平臺兩方面的型別安全。程式碼生成工具使用 JavaScript 元件宣告作為唯一事實源,生成 C++ 結構體來持有 props 屬性。不會因為 JavaScript 和宿主元件 props 屬性不匹配而出現構建錯誤。
  • 共享 C++ core:渲染器是用 C++ 實現的,其核心 core 在平臺之間是共享的。這增加了一致性並且使得新的平臺能夠更容易採用 React Native。(譯註:例如 VR 新平臺)
  • 更好的宿主平臺互操作性:當宿主元件整合到 React Native 時,同步和執行緒安全的佈局計算提升了使用者體驗(譯註:沒有非同步的抖動)。這意味著那些需要同步 API 的宿主平臺庫,變得更容易整合了。
  • 效能提升:新的渲染系統的實現是跨平臺的,每個平臺都從那些原本只在某個特定平臺的實現的效能優化中,得到了收益。比如拍平檢視層級,原本只是 Android 上的效能優化方案,現在 Android 和 iOS 都直接有了。
  • 一致性:新的渲染系統的實現是跨平臺的,不同平臺之間更容易保持一致。
  • 更快的啟動速度:預設情況下,宿主元件的初始化是懶執行的。
  • JS 和宿主平臺之間的資料序列化更少:React 使用序列化 JSON 在 JavaScript 和宿主平臺之間傳遞資料。新的渲染器用 JSI(JavaScript Interface)直接獲取 JavaScript 資料。

名詞解釋

JavaScript Interfaces (JSI):一個輕量級的 API,給在 C++ 應用中嵌入的 JavaScript 引擎用的。Fabric 使用它在 Fabric 的 C++ 核心和 React 之間進行通訊。

渲染、提交和掛載

React Native 渲染器通過一系列加工處理,將 React 程式碼渲染到宿主平臺。這一系列加工處理就是渲染流水線(pipeline),它的作用是初始化渲染和 UI 狀態更新。 接下來介紹的是渲染流水線,及其在各種場景中的不同之處。

(譯註:pipeline 的原義是將計算機指令處理過程拆分為多個步驟,並通過多個硬體處理單元並行執行來加快指令執行速度。其具體執行過程類似工廠中的流水線,並因此得名。)

渲染流水線可大致分為三個階段:

  • 渲染(Render):在 JavaScript 中,React 執行那些產品邏輯程式碼建立 React 元素樹(React Element Trees)。然後在 C++ 中,用 React 元素樹建立 React 影子樹(React Shadow Tree)。
  • 提交(Commit):在 React 影子樹完全建立後,渲染器會觸發一次提交。這會將 React 元素樹和新建立的 React 影子樹的提升為“下一棵要掛載的樹”。 這個過程中也包括了佈局資訊計算。
  • 掛載(Mount):React 影子樹有了佈局計算結果後,它會被轉化為一個宿主檢視樹(Host View Tree)。

名詞解釋

React 元素樹(React Element Trees):React 元素樹是通過 JavaScript 中的 React 建立的,該樹由一系類 React 元素組成。一個 React 元素就是一個普通的 JavaScript 物件,它描述了應該在螢幕中展示什麼。一個元素包括屬性 props、樣式 styles、子元素 children。React 元素分為兩類:React 複合元件例項(React Composite Components)和 React 宿主元件(React Host Components)例項,並且它只存在於 JavaScript 中。

React 影子樹(React Shadow Tree):React 影子樹是通過 Fabric 渲染器建立的,樹由一系列 React 影子節點組成。一個 React 影子節點是一個物件,代表一個已經掛載的 React 宿主元件,其包含的屬性 props 來自 JavaScript。它也包括佈局資訊,比如座標系 x、y,寬高 width、height。在新渲染器 Fabric 中,React 影子節點物件只存在於 C++ 中。而在老架構中,它存在於手機執行時的堆疊中,比如 Android 的 JVM。

宿主檢視樹(Host View Tree):宿主檢視樹就是一系列的宿主檢視。宿主平臺有 Android 平臺、iOS 平臺等等。在 Android 上,宿主檢視就是 android.view.ViewGroup例項、 android.widget.TextView例項等等。宿主檢視就像積木一樣地構成了宿主檢視樹。每個宿主檢視的大小和座標位置基於的是 LayoutMetrics,而 LayoutMetrics是通過佈局引擎 Yoga 計算出來的。宿主檢視的樣式和內容資訊,是從 React 影子樹中得到的。

渲染流水線的各個階段可能發生在不同的執行緒中,更詳細的資訊可以參考執行緒模型部分。

React Native renderer Data flow

渲染流水線存在三種不同場景:

  1. 初始化渲染
  2. React 狀態更新
  3. React Native 渲染器的狀態更新

初始化渲染

渲染階段

想象一下你準備渲染一個元件:

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

// <MyComponent />

在上面的例子中, <MyComponent />是 React 元素。React 會將 React 元素簡化為最終的 React 宿主元件。每一次都會遞迴地呼叫函式元件 MyComponet ,或類元件的 render 方法,直至所有的元件都被呼叫過。現在,你擁有一棵 React 宿主元件的 React 元素樹。

Phase one: render

名詞解釋:

React 元件(React Component):React 元件就是 JavaScript 函式或者類,描述如何建立 React 元素。

React 複合元件(React Composite Components):React 元件的 render 方法中,包括其他 React 複合元件和 React 宿主元件。(譯註:複合元件就是開發者宣告的元件)

React 宿主元件(React Host Components):React 元件的檢視是通過宿主檢視,比如 <View><Text>,實現的。在 Web 中,ReactDOM 的宿主元件就是 <p>標籤、<div>標籤代表的元件。

在元素簡化的過程中,每呼叫一個 React 元素,渲染器同時會同步地建立 React 影子節點。這個過程只發生在 React 宿主元件上,不會發生在 React 複合元件上。比如,一個 <View>會建立一個 ViewShadowNode 物件,一個<Text>會建立一個TextShadowNode物件。注意,<MyComponent>並沒有直接對應的 React 影子節點。

在 React 為兩個 React 元素節點建立一對父子關係的同時,渲染器也會為對應的 React 影子節點建立一樣的父子關係。這就是影子節點的組裝方式。

其他細節

  • 建立 React 影子節點、建立兩個影子節點的父子關係的操作是同步的,也是執行緒安全的。該操作的執行是從 React(JavaScript)到渲染器(C++)的,大部分情況下是在 JavaScript 執行緒上執行的。(譯註:後面執行緒模型有解釋)
  • React 元素樹和元素樹中的元素並不是一直存在的,它只一個當前檢視的描述,而最終是由 React “fiber” 來實現的。每一個 “fiber” 都代表一個宿主元件,存著一個 C++ 指標,指向 React 影子節點。這些都是因為有了 JSI 才有可能實現的。學習更多關於 “fibers” 的資料參考這篇文件
  • React 影子樹是不可變的。為了更新任意的 React 影子節點,渲染器會建立了一棵新的 React 影子樹。為了讓狀態更新更高效,渲染器提供了 clone 操作。更多細節可參考後面的 React 狀態更新部分。

在上面的示例中,各個渲染階段的產物如圖所示:

Step one

提交階段

在 React 影子樹建立完成後,渲染器觸發了一次 React 元素樹的提交。Phase two: commit

提交階段(Commit Phase)由兩個操作組成:佈局計算和樹的提升。

  • 佈局計算(Layout Calculation):這一步會計算每個 React 影子節點的位置和大小。在 React Native 中,每一個 React 影子節點的佈局都是通過 Yoga 佈局引擎來計算的。實際的計算需要考慮每一個 React 影子節點的樣式,該樣式來自於 JavaScript 中的 React 元素。計算還需要考慮 React 影子樹的根節點的佈局約束,這決定了最終節點能夠擁有多少可用空間。

Step two

  • 樹提升,從新樹到下一棵樹(Tree Promotion,New Tree → Next Tree):這一步會將新的 React 影子樹提升為要掛載的下一棵樹。這次提升代表著新樹擁有了所有要掛載的資訊,並且能夠代表 React 元素樹的最新狀態。下一棵樹會在 UI 執行緒下一個“tick”進行掛載。(譯註:tick 是 CUP 的最小時間單元)

更多細節

  • 這些操作都是在後臺執行緒中非同步執行的。
  • 絕大多數佈局計算都是 C++ 中執行,只有某些元件,比如 Text、TextInput 元件等等,的佈局計算是在宿主平臺執行的。文字的大小和位置在每個宿主平臺都是特別的,需要在宿主平臺層進行計算。為此,Yoga 佈局引擎呼叫了宿主平臺的函式來計算這些元件的佈局。

掛載階段

Phase two: commit

掛載階段(Mount Phase)會將已經包含佈局計算資料的 React 影子樹,轉換為以畫素形式渲染在螢幕中的宿主檢視樹。請記住,這棵 React 元素樹看起來是這樣的:

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

站在更高的抽象層次上,React Native 渲染器為每個 React 影子節點建立了對應的宿主檢視,並且將它們掛載在螢幕上。在上面的例子中,渲染器為<View> 建立了android.view.ViewGroup 例項,為 <Text> 建立了文字內容為“Hello World”的 android.widget.TextView例項 。iOS 也是類似的,建立了一個 UIView 並呼叫 NSLayoutManager 建立文字。然後會為宿主檢視配置來自 React 影子節點上的屬性,這些宿主檢視的大小位置都是通過計算好的佈局資訊配置的。

Step two

更詳細地說,掛載階段由三個步驟組成:

  • 樹對比(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>
  );
}

應用我們在初始化渲染部分學的知識,你可以得到如下的三棵樹:

Render pipeline 4

請注意,節點 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 影子樹,而是必須為每棵樹建立一個包含新屬性、新樣式和新子節點的新副本。

讓我們繼續探究狀態更新時,渲染流水線的各個階段發生了什麼。

渲染階段

Phase one: render

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' 代表“新樹”。

Render pipeline 5

注意節點 4 在 T and T' 之間是共享的。結構共享提升了效能並減少了記憶體的使用。

提交階段

Phase two: commit

在 React 建立完新的 React 元素樹和 React 影子樹後,需要提交它們。

  • 佈局計算(Layout Calculation):狀態更新時的佈局計算,和初始化渲染的佈局計算類似。一個重要的不同之處是佈局計算可能會導致共享的 React 影子節點被複制。這是因為,如果共享的 React 影子節點的父節點引起了佈局改變,共享的 React 影子節點的佈局也可能發生改變。
  • 樹提升(Tree Promotion ,New Tree → Next Tree): 和初始化渲染的樹提升類似。
  • 樹對比(Tree Diffing): 這個步驟會計算“先前渲染的樹”(T)和“下一棵樹”(T')的區別。計算的結果是原生檢視的變更操作。

    • 在上面的例子中,這些操作包括:UpdateView(**'Node 3'**, {backgroundColor: 'yellow'})

掛載階段

Phase three: mount

  • 樹提升(Tree Promotion ,Next Tree → Rendered Tree): 在這個步驟中,會自動將“下一棵樹”提升為“先前渲染的樹”,因此在下一個掛載階段,樹的對比計算用的是正確的樹。
  • 檢視掛載(View Mounting):這個步驟會在對應的原生檢視上執行原子變更操作。在上面的例子中,只有檢視3(View 3)的背景顏色會更新,變為黃色。

Render pipeline 6

React Native 渲染器狀態更新

對於影子樹中的大多數資訊而言,React 是唯一所有方也是唯一事實源。並且所有來源於 React 的資料都是單向流動的。

但有一個例外。這個例外是一種非常重要的機制:C++ 元件可以擁有狀態,且該狀態可以不直接暴露給 JavaScript,這時候 JavaScript (或 React)就不是唯一事實源了。通常,只有複雜的宿主元件才會用到 C++ 狀態,絕大多數宿主元件都不需要此功能。

例如,ScrollView 使用這種機制讓渲染器知道當前的偏移量是多少。偏移量的更新是宿主平臺的觸發,具體地說是 ScrollView 元件。這些偏移量資訊在 React Native 的 measure 等 API 中有用到。 因為偏移量資料是由 C++ 狀態持有的,所以源於宿主平臺更新,不影響 React 元素樹。

從概念上講,C++ 狀態更新類似於我們前面提到的 React 狀態更新,但有兩點不同:

  • 因為不涉及 React,所以跳過了“渲染階段”(Render phase)。
  • 更新可以源自和發生在任何執行緒,包括主執行緒。

Phase two: commit

提交階段(Commit Phase):在執行 C++ 狀態更新時,會有一段程式碼把影子節點(N)的 C++ 狀態設定為值 S。React Native 渲染器會反覆嘗試獲取 N 的最新提交版本,並使用新狀態 S 複製它 ,並將新的影子節點 N' 提交給影子樹。如果 React 在此期間執行了另一次提交,或者其他 C++ 狀態有了更新,本次 C++ 狀態提交失敗。這時渲染器將多次重試 C++ 狀態更新,直到提交成功。這可以防止真實源的衝突和競爭。

Phase three: mount

掛載階段(Mount Phase)實際上與 React 狀態更新的掛載階段相同。渲染器仍然需要重新計算佈局、執行樹對比等操作。詳細步驟在前面已經講過了。

跨平臺實現

React Native 渲染器使用 C++ core 渲染實現了跨平臺共享。

在上一代 React Native 渲染器中,React 影子樹、佈局邏輯、檢視拍平演算法是在各個平臺單獨實現的。當前的渲染器的設計上採用的是跨平臺的解決方案,共享了核心的 C++ 實現。

React Native 團隊計劃將動畫系統加入到渲染系統中,並將 React Native 的渲染系統擴充套件到新的平臺,例如 Windows、遊戲機、電視等等。

使用 C++ 作為核心渲染系統有幾個有點。首先,單一實現降低了開發和維護成本。其次,它提升了建立 React 影子樹的效能,同時在 Android 上,也因為不再使用 JNI for Yoga,降低了 Yoga 渲染引擎的開銷,佈局計算的效能也有所提升。最後,每個 React 影子節點在 C++ 中佔用的記憶體,比在 Kotlin 或 Swift 中佔用的要小。

名詞解釋

Java Native Interface (JNI):一個用 Java 寫的 API,用於在 Java 中寫 native(譯註:指呼叫 C++) 方法。作用是實現 Fabric 的 C++ 核心和 Android 的通訊。

React Native 團隊還使用了強制不可變的 C++ 特性,來確保併發訪問時共享資源即便不加鎖保護,也不會有問題。

但在 Android 端還有兩種例外,渲染器依然會有 JNI 的開銷:

  • 複雜檢視,比如 Text、TextInput 等,依然會使用 JNI 來傳輸屬性 props。
  • 在掛載階段依然會使用 JNI 來傳送變更操作。

React Native 團隊在探索使用 ByteBuffer 序列化資料這種新的機制,來替換 ReadableMap,減少 JNI 的開銷。目標是將 JNI 的開銷減少 35~50%。

渲染器提供了 C++ 與兩邊通訊的 API:

  • (i) 與 React 通訊
  • (ii) 與宿主平臺通訊

關於 (i) React 與渲染器的通訊,包括渲染(render) React 樹和監聽事件(event),比如 onLayoutonKeyPress、touch 等。

關於 (ii) React Native 渲染器與宿主平臺的通訊,包括在螢幕上掛載(mount)宿主檢視,包括 create、insert、update、delete 宿主檢視,和監聽使用者在宿主平臺產生的事件(event)

Cross-platform implementation diagram

檢視拍平

檢視拍平(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 在渲染時,會生成以下三棵樹:

Diagram one

注意檢視 2 和檢視 3 是“只參與佈局”的檢視,因為它們在螢幕上渲染只是為了提供 10 畫素的外邊距。

為了提升 React 元素樹中“只參與佈局”型別的效能,渲染器實現了一種檢視拍平的機制來合併或拍平這類節點,減少螢幕中宿主檢視的層級深度。該演算法考慮到了如下屬性,比如 margin, padding, backgroundColor, opacity等等。

檢視拍平演算法是渲染器的對比(diffing)階段的一部分,這樣設計的好處是我們不需要額外的 CUP 耗時,來拍平 React 元素樹中“只參與佈局”的檢視。此外,作為 C++ 核心的一部分,檢視拍平演算法預設是全平臺共用的。

在前面的例子中,檢視 2 和檢視 3 會作為“對比演算法”(diffing algorithm)的一部分被拍平,而它們的樣式結果會被合併到檢視 1 中。

Diagram two

雖然,這種優化讓渲染器少建立和渲染兩個宿主檢視,但從使用者的角度看螢幕內容沒有任何區別。

執行緒模型

React Native 渲染器在多個執行緒之間分配渲染流水線(render pipeline)任務。

接下來我們會給執行緒模型下定義,並提供一些示例來說明渲染流水線的執行緒用法。

React Native 渲染器是執行緒安全的。從更高的視角看,在框架內部執行緒安全是通過不可變的資料結果保障的,其使用的是 C++ 的 const correctness 特性。這意味著,在渲染器中 React 的每次更新都會重新建立或複製新物件,而不是更新原有的資料結構。這是框架把執行緒安全和同步 API 暴露給 React 的前提。

渲染器使用三個不同的執行緒:

  • UI 執行緒(主執行緒):唯一可以操作宿主檢視的執行緒。
  • JavaScript 執行緒:這是執行 React 渲染階段的地方。
  • 後臺執行緒:專門用於佈局的執行緒。

讓我們回顧一下每個階段支援的執行場景:

Threading model symbols

渲染場景

在後臺執行緒中渲染

這是最常見的場景,大多數的渲染流水線發生在 JavaScript 執行緒和後臺執行緒。

Threading model use case one

在主執行緒中渲染

當 UI 執行緒上有高優先順序事件時,渲染器能夠在 UI 執行緒上同步執行所有渲染流水線。

Threading model use case two

預設或連續事件中斷

在這個場景中,UI 執行緒的低優先順序事件中斷了渲染步驟。React 和 React Native 渲染器能夠中斷渲染步驟,並把它的狀態和一個在 UI 執行緒執行的低優先順序事件合併。在這個例子中渲染過程會繼續在後臺執行緒中執行。

Threading model use case three

不相干的事件中斷

渲染步驟是可中斷的。在這個場景中, UI 執行緒的高優先順序事件中斷了渲染步驟。React 和渲染器是能夠打斷渲染步驟的,並把它的狀態和 UI 執行緒執行的高優先順序事件合併。在 UI 執行緒渲染步驟是同步執行的。

Threading model use case four

來自 JavaScript 執行緒的後臺執行緒批量更新

在後臺執行緒將更新分派給 UI 執行緒之前,它會檢查是否有新的更新來自 JavaScript。 這樣,當渲染器知道新的狀態要到來時,它就不會直接渲染舊的狀態。

Threading model use case five

C++ 狀態更新

更新來自 UI 執行緒,並會跳過渲染步驟。更多細節請參考 React Native 渲染器狀態更新。

Threading model use case six

相關文章