banner
IWSR

IWSR

我永远喜欢志喜屋梦子!

React Suspense のソースコード解析

説明#

  1. 表紙の画像は我が家の猫の妹です。

  2. React バージョン v18.1.0

  3. React.lazy で導入されたコンポーネントを lazyComponent と呼び、Suspense の fallback 内のコンポーネントを fallbackComponent と呼びます。

  4. 以下のコードに基づいて分析します。

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 にのみ注目すればよいです。

OtherComponent

最初のパス#

タイトルは updateSuspenseComponent 内のコメントから取られています。この関数は beginWork が Suspense ノードに達したときに呼び出され、その内部ロジックは First pass と Second pass でそれぞれ異なるロジックを持っています(The second pass では LazyComponent の fallback に対応するノードが作成されます)。しかし、The first pass ではまず offScreen を作成します。文中の例は First pass フェーズを経た後、以下のような構造の fiber ツリーを得ることができます。

fiber

この中で lazy という fiber は React.lazy で導入された OtherComponent に対応しており、この時点ではまだプレースホルダーに過ぎず、コンポーネントとは呼べません。beginWork が lazy に達したときに初めて導入コンポーネントのロジックが呼び出されます(コードは mountLazyComponent にあります)。

上記のコードブロックで ctor に対応する関数

ctor

対応するコールスタック

image

エラーキャッチ —— throwException#

エラーキャッチは renderRoot の handleError 内で発生し、この関数内の throwException はまず lazy fiber に Incomplete のマークを付け、最近の Suspense ノードを見つけて shouldCapture のマークを付けます(このマークは The second pass 内で作用します)。 lazy fiber から上に遡って通過したすべての fiber ノードに Incomplete マークが付けられ、Suspense ノードに達するまで続きます(Suspense ノードは Incomplete 以外に shouldCapture マークも付けられます)。

image

handleError 全体を分析するのは長くなるため、以下では重要なステップのみを抜粋して証明します。

lazyComponent に Incomplete マークを付ける#

image

最近の SuspenseComponent を見つけて shouldCapture マークを付ける#

image

image

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 内の大まかなロジック

image

上記のロジックを経た後、その構造は以下のようになります。

image

The second pass が終了した後、その fiber ツリー構造は以下のようになります。

image

promise resolve 後にトリガーされる再レンダリングについて#

非同期コンポーネントの読み込みが終了した後、つまり以前の pending の promise インスタンスの状態が変わった後、更新をトリガーするために対応する成功コールバックを追加する必要があります。このコールバック関数は commit フェーズ(commitRootImpl)内の mutation 小フェーズ(commitMutationEffects)内で promise に登録され、具体的な位置は attachSuspenseRetryListeners 内に存在します。

image

図中の retry 関数は resolveRetryWakeable(位置は ReactFiberWorkLoop.old.js 内)です。

image-20230621015020524

ensureRootIsScheduled に関連するものは私の React Hooks: useState 分析 の解析を参照してください。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。