説明#
-
表紙の画像は我が家の猫の妹です。
-
React バージョン v18.1.0
-
React.lazy で導入されたコンポーネントを lazyComponent と呼び、Suspense の fallback 内のコンポーネントを fallbackComponent と呼びます。
-
以下のコードに基づいて分析します。
TLNR#
beginWork で fiber を作成する過程で初めて lazyComponent に遭遇した際、pending 状態の promise が throw されます。この promise オブジェクトは外部の try...catch...(sync モードでは renderRootSync 内、concurrent モードでは renderRootConcurrent 内)で捕捉され、その後上に遡って通過したすべての fiber ノードに Incomplete タグが付けられ、Suspense ノードに達するまで(completeUnitOfWork)行われます。その後、そのノードから再び下に向かって beginWork で fiber を作成し、この時に Suspense の fallback ノードが作成されます。その後、mutation フェーズ(commitMutationEffects)で以前に throw された promise に成功コールバックが追加され、非同期コンポーネントが成功裏に読み込まれた後に再レンダリングをサポートします。
React.lazy#
React.lazy の Suspense に関する分析はそれほど重要なポイントではないため、ts 宣言を参考にして引数と戻り値の型を理解するだけで十分です。
サンプルコードの OtherComponent に打点を入れると、その値をより直感的に見ることができ、その中で $$typeof にのみ注目すればよいです。
最初のパス#
タイトルは updateSuspenseComponent 内のコメントから取られています。この関数は beginWork が Suspense ノードに達したときに呼び出され、その内部ロジックは First pass と Second pass でそれぞれ異なるロジックを持っています(The second pass では LazyComponent の fallback に対応するノードが作成されます)。しかし、The first pass ではまず offScreen を作成します。文中の例は First pass フェーズを経た後、以下のような構造の fiber ツリーを得ることができます。
この中で lazy という fiber は React.lazy で導入された OtherComponent に対応しており、この時点ではまだプレースホルダーに過ぎず、コンポーネントとは呼べません。beginWork が lazy に達したときに初めて導入コンポーネントのロジックが呼び出されます(コードは mountLazyComponent にあります)。
上記のコードブロックで ctor に対応する関数
対応するコールスタック
エラーキャッチ —— throwException#
エラーキャッチは renderRoot の handleError 内で発生し、この関数内の throwException はまず lazy fiber に Incomplete のマークを付け、最近の Suspense ノードを見つけて shouldCapture のマークを付けます(このマークは The second pass 内で作用します)。 lazy fiber から上に遡って通過したすべての fiber ノードに Incomplete マークが付けられ、Suspense ノードに達するまで続きます(Suspense ノードは Incomplete 以外に shouldCapture マークも付けられます)。
handleError 全体を分析するのは長くなるため、以下では重要なステップのみを抜粋して証明します。
lazyComponent に Incomplete マークを付ける#
最近の SuspenseComponent を見つけて shouldCapture マークを付ける#
throwException 内のいくつかの小さな詳細#
ConcurrentMode 下の attachPingListener#
attachPingListener は ConcurrentMode 下でのみ呼び出されます。非同期コンポーネントのリクエストは、React が commit フェーズに入るときに返される可能性があるため、ConcurrentMode モードでは高優先度のタスクが割り込むことが許可されています。リクエストに対応する優先度が進行中のタスクよりも高い場合、割り込んで処理されます。
attachRetryListener#
attachRetryListener は主に pending 状態の promise を Suspense fiber の updateQueue に掛けるもので、他の fiber とは少し異なります(結局のところ、すべて update インスタンスを装填するための連結リストです)。
エラーキャッチ —— completeUnitOfWork#
throwException の後に completeUnitOfWork が呼び出されます。この関数の役割は、lazy コンポーネントから上に遡って通過したすべての fiber ノードに Incomplete マークを付けることですが、ShouldCapture マークの付いたノードに遭遇すると、そのノードに関するエラー処理のマークを消去します(next.flags &= HostEffectMask)し、そのノードから beginWork に入ります。これは completeWork のエントリ関数でもあり、React の重要な関数です。
The second pass#
completeUnitOfWork の処理を経た後、Suspense ノードには DidCapture マークが付けられ、The second pass では beginWork が再び Suspense ノードから fiber ノードを作成します(updateSuspenseComponent)。DidCapture の影響により、updateSuspenseComponent は first pass でスキップされたロジックに入り、fallback の内容を処理します。
mountSuspenseFallbackChildren 内の大まかなロジック
上記のロジックを経た後、その構造は以下のようになります。
The second pass が終了した後、その fiber ツリー構造は以下のようになります。
promise resolve 後にトリガーされる再レンダリングについて#
非同期コンポーネントの読み込みが終了した後、つまり以前の pending の promise インスタンスの状態が変わった後、更新をトリガーするために対応する成功コールバックを追加する必要があります。このコールバック関数は commit フェーズ(commitRootImpl)内の mutation 小フェーズ(commitMutationEffects)内で promise に登録され、具体的な位置は attachSuspenseRetryListeners 内に存在します。
図中の retry 関数は resolveRetryWakeable(位置は ReactFiberWorkLoop.old.js 内)です。
ensureRootIsScheduled に関連するものは私の React Hooks: useState 分析 の解析を参照してください。