レンダリング、コミット、およびマウント
このドキュメントは、現在展開中の新しいアーキテクチャを参照しています。
React Nativeレンダラーは、Reactロジックをホストプラットフォームにレンダリングするために、一連の作業を実行します。この一連の作業はレンダリングパイプラインと呼ばれ、最初のレンダリングとUI状態の更新で発生します。このドキュメントでは、レンダリングパイプラインとそのシナリオにおける違いについて説明します。
レンダリングパイプラインは、一般的に3つのフェーズに分けられます。
- **レンダリング:** Reactは製品ロジックを実行し、JavaScriptでReact要素ツリーを作成します。このツリーから、レンダラーはC++でReactシャドウツリーを作成します。
- **コミット:** Reactシャドウツリーが完全に作成されると、レンダラーはコミットをトリガーします。これは、React要素ツリーと新しく作成されたReactシャドウツリーの両方を、「次のツリー」としてマウントされるように**昇格**させます。これにより、レイアウト情報の計算もスケジュールされます。
- **マウント:** レイアウト計算の結果を含むReactシャドウツリーは、ホストビューツリーに変換されます。
レンダリングパイプラインのフェーズは、異なるスレッドで発生する可能性があります。詳細については、スレッドモデルのドキュメントを参照してください。
最初のレンダリング
次のようにレンダリングしたいとします。
function MyComponent() {
return (
<View>
<Text>Hello, World</Text>
</View>
);
}
// <MyComponent />
上記の例では、<MyComponent />
はReact要素です。Reactは、このReact要素を、それを呼び出すこと(またはJavaScriptクラスで実装されている場合はその`render`メソッドを呼び出すこと)によって、すべてのReact要素をそれ以上削減できなくなるまで、ターミナルReactホストコンポーネントに再帰的に削減します。これで、ReactホストコンポーネントのReact要素ツリーができました。
フェーズ1. レンダリング
この要素の削減プロセスにおいて、各React要素が呼び出されるたびに、レンダラーは同期的にReactシャドウノードも作成します。これは、React複合コンポーネントではなく、Reactホストコンポーネントに対してのみ発生します。上記の例では、<View>
はViewShadowNode
オブジェクトの作成につながり、<Text>
はTextShadowNode
オブジェクトの作成につながります。注目すべきは、<MyComponent>
を直接表すReactシャドウノードは決して存在しないということです。
Reactが2つのReact要素ノード間に親子関係を作成するたびに、レンダラーは対応するReactシャドウノード間に同じ関係を作成します。これが、Reactシャドウツリーが組み立てられる方法です。
追加の詳細
- これらの操作(Reactシャドウノードの作成、2つのReactシャドウノード間の親子関係の作成)は、通常はJavaScriptスレッドで、React(JavaScript)からレンダラー(C++)に実行される同期的でスレッドセーフな操作です。
- React要素ツリー(とその構成要素であるReact要素ノード)は、無期限に存在するわけではありません。これは、Reactの「ファイバー」によって具現化された一時的な表現です。ホストコンポーネントを表す各「ファイバー」は、JSIによって可能になった、ReactシャドウノードへのC++ポインタを格納します。このドキュメントで「ファイバー」の詳細をご覧ください。
- Reactシャドウツリーは不変です。任意のReactシャドウノードを更新するには、レンダラーは新しいReactシャドウツリーを作成します。ただし、レンダラーはクローン操作を提供して、状態の更新をより高性能にします(詳細についてはReact状態の更新を参照してください)。
上記の例では、レンダリングフェーズの結果は次のようになります。
Reactシャドウツリーが完成すると、レンダラーはReact要素ツリーのコミットをトリガーします。
フェーズ2. コミット
コミットフェーズは、レイアウト計算とツリーの昇格の2つの操作で構成されます。
- **レイアウト計算:** この操作は、各Reactシャドウノードの位置とサイズを計算します。React Nativeでは、これにはYogaを呼び出して各Reactシャドウノードのレイアウトを計算することが含まれます。実際の計算には、JavaScriptのReact要素に由来する各Reactシャドウノードのスタイルが必要です。また、結果のノードが占有できる利用可能なスペースの量を決定する、Reactシャドウツリーのルートのレイアウト制約も必要です。
- **ツリーの昇格(新しいツリー→次のツリー):** この操作は、新しいReactシャドウツリーをマウントされる「次のツリー」として昇格させます。この昇格は、新しいReactシャドウツリーがマウントされるすべての情報を持っており、React要素ツリーの最新の状況を表していることを示します。 「次のツリー」は、UIスレッドの次の「ティック」でマウントされます。
追加の詳細
- これらの操作は、バックグラウンドスレッドで非同期的に実行されます。
- レイアウト計算の大部分は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シャドウノードのプロップを使用するように構成され、そのサイズと位置は計算されたレイアウト情報を使用して構成されます。
より詳細には、マウントフェーズは次の3つのステップで構成されています。
- **ツリーの差分:** このステップは、「以前にレンダリングされたツリー」と「次のツリー」の間の差分をC++で完全に計算します。結果は、ホストビューに対して実行されるべき原子的な変更操作のリストです(例:
createView
、updateView
、removeView
、deleteView
など)。このステップは、不要なホストビューの作成を回避するためにReactシャドウツリーがフラット化される場所でもあります。ビューのフラット化で、このアルゴリズムの詳細をご覧ください。 - **ツリーの昇格(次のツリー→レンダリングされたツリー):** このステップは、「次のツリー」を「以前にレンダリングされたツリー」に原子的に昇格させるため、次のマウントフェーズは適切なツリーに対して差分を計算します。
- **ビューのマウント:** このステップは、対応するホストビューに原子的な変更操作を適用します。このステップは、UIスレッドのホストプラットフォームで実行されます。
追加の詳細
- 操作はUIスレッドで同期的に実行されます。コミットフェーズがバックグラウンドスレッドで実行される場合、マウントフェーズはUIスレッドの次の「ティック」でスケジュールされます。一方、コミットフェーズが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要素ツリーとReactシャドウツリーを直接変更するのではなく、Reactは新しいプロップ、スタイル、および子を含む各ツリーの新しいコピーを作成する必要があります。
状態更新中のレンダリングパイプラインの各フェーズについて調べてみましょう。
フェーズ1. レンダリング
Reactが新しい状態を含む新しいReact要素ツリーを作成すると、変更の影響を受けるすべてのReact要素とReactシャドウノードをクローンする必要があります。クローン作成後、新しいReactシャドウツリーがコミットされます。
React Nativeレンダラーは、構造的共有を活用して、不変性のオーバーヘッドを最小限に抑えます。React要素が新しい状態を含むようにクローンされると、ルートまでのパス上にあるすべてのReact要素がクローンされます。Reactは、プロップ、スタイル、または子の更新が必要な場合にのみ、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シャドウツリーを作成した後、それらをコミットする必要があります。
- レイアウト計算: 初期レンダリング中のレイアウト計算と同様です。重要な違いの1つは、レイアウト計算により、共有されたReactシャドウノードが複製される可能性があることです。これは、共有されたReactシャドウノードの親がレイアウト変更を受けると、共有されたReactシャドウノードのレイアウトも変更される可能性があるためです。
- ツリー昇格 (新しいツリー→次のツリー): 初期レンダリング中のツリー昇格と同様です。
フェーズ3. マウント
- **ツリーの昇格(次のツリー→レンダリングされたツリー):** このステップは、「次のツリー」を「以前にレンダリングされたツリー」に原子的に昇格させるため、次のマウントフェーズは適切なツリーに対して差分を計算します。
- ツリーディファリング: このステップでは、「以前レンダリングされたツリー」(T)と「次のツリー」(T')の違いを計算します。結果は、ホストビューに対して実行されるべきアトミックな変更操作のリストです。
- 上記の例では、操作は
UpdateView(**ノード3**, {backgroundColor: 'yellow'})
で構成されます。 - 差分は、現在マウントされている任意のツリーと任意の新しいツリーについて計算できます。レンダラーは、ツリーの中間バージョンの一部をスキップできます。
- 上記の例では、操作は
- ビューのマウント: このステップでは、対応するホストビューにアトミックな変更操作を適用します。上記の例では、ビュー3の
backgroundColor
のみが(黄色に)更新されます。
React Nativeレンダラーの状態更新
シャドウツリー内のほとんどの情報について、Reactは単一の所有者であり、唯一の真実の源です。すべてのデータはReactから発信され、データの流れは一方向です。
ただし、1つの例外と重要なメカニズムがあります。C++のコンポーネントには、JavaScriptに直接公開されていない状態が含まれる可能性があり、JavaScriptが真実の源ではありません。C++とホストプラットフォームが、このC++状態を制御します。一般的に、これは、C++状態を必要とする複雑なホストコンポーネントを開発する場合にのみ関連します。ほとんどのホストコンポーネントはこの機能を必要としません。
たとえば、ScrollView
は、このメカニズムを使用して、現在のオフセットをレンダラーに知らせます。更新はホストプラットフォームから、具体的にはScrollView
コンポーネントを表すホストビューからトリガーされます。オフセットに関する情報は、measureのようなAPIで使用されます。この更新はホストプラットフォームから発生し、React要素ツリーに影響を与えないため、この状態データはC++状態によって保持されます。
概念的には、C++状態の更新は、上記で説明したReactの状態更新と同様です。2つの重要な違いがあります。
- それらはReactが関与していないため、「レンダリングフェーズ」をスキップします。
- 更新は、メインスレッドを含む任意のスレッドで開始および発生する可能性があります。
フェーズ2. コミット
C++状態の更新を実行する場合、コードブロックはShadowNode
(N)の更新を要求して、C++状態を値Sに設定します。React Nativeレンダラーは、Nの最新のコミットされたバージョンを取得し、新しい状態Sを使用して複製し、N’をツリーにコミットする処理を繰り返し試みます。この間にReactまたは別のC++状態の更新が別のコミットを実行した場合、C++状態のコミットは失敗し、レンダラーはコミットが成功するまでC++状態の更新を何度も再試行します。これにより、真実の源の衝突と競合状態を防ぎます。
フェーズ3. マウント
マウントフェーズは、Reactの状態更新のマウントフェーズと事実上同一です。レンダラーは、レイアウトの再計算、ツリーディファリングなどを実行する必要があります。詳細は上記のセクションを参照してください。