一.UI レイヤーの try...catch
まず結論を述べます。Suspense は try...catch のように、UI が安全かどうかを決定します:
try {
// 一旦 ready ではないものがある
} catch {
// 直ちに catch ブロックに入り、fallback へ
}
では、どのように安全を定義するのでしょうか?
考えてみてください。もしコンポーネントのコードがまだロード完了していないのに、それをレンダリングしようとしたら、明らかに安全ではありません。したがって、狭義にはコンポーネントコードが準備完了しているコンポーネントは安全であると考えます。同期コンポーネントとロード完了した非同期コンポーネント(React.lazy)を含みます。例えば:
// 同期コンポーネント、安全
import OtherComponent from './OtherComponent';
// 非同期コンポーネント、不安全
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
// ...AnotherComponent のコードロード完了後
// ロード完了した非同期コンポーネント、安全
AnotherComponent
Error Boundary
似たものに Error Boundary があります。これも UI レイヤーの try...catch の一種で、その安全の定義はコンポーネントコードの実行に JavaScript Error がないことです:
Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed.
これら 2 つの定義は衝突しないことがわかります。実際、Suspense と Error Boundary は確かに共存できます。例えば Error Boundary を通じて非同期コンポーネントのロードエラーをキャッチします:
If the other module fails to load (for example, due to network failure), it will trigger an error. You can handle these errors to show a nice user experience and manage recovery with Error Boundaries.
例えば:
import MyErrorBoundary from './MyErrorBoundary';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
const MyComponent = () => (
<div>
<MyErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</MyErrorBoundary>
</div>
);
二.Suspense を手搓りする
冒頭の 5 行コードは面白いかもしれませんが、まだ十分明確ではありません。続けて埋めます:
function Suspense(props) {
const { children, fallback } = props;
try {
// 一旦 ready ではないものがある
React.Children.forEach(children, function() {
assertReady(this);
});
} catch {
// 直ちに catch ブロックに入り、fallback へ
return fallback;
}
return children;
}
assertReady はアサーションで、安全でないコンポーネントに対して Error を投げます:
import { isLazy } from "react-is";
function assertReady(element) {
// 尚未加载完成的 Lazy 组件不安全
if (isLazy(element) && element.type._status !== 1) {
throw new Error('Not ready yet.');
}
}
P.S.react-is は Lazy コンポーネントを区別するために使用され、_status は Lazy コンポーネントのロード状態を表します。詳細は React Suspense | 具体实现 を参照
試玩一下:
function App() {
return (<>
<Suspense fallback={<div>loading...</div>}>
<p>Hello, there.</p>
</Suspense>
<Suspense fallback={<div>loading...</div>}>
<LazyComponent />
</Suspense>
<Suspense fallback={<div>loading...</div>}>
<ReadyLazyComponent />
</Suspense>
<Suspense fallback={<div>loading...</div>}>
<p>Hello, there.</p>
<LazyComponent />
<ReadyLazyComponent />
</Suspense>
</>);
}
対応するインターフェース内容は:
Hello, there.
loading...
ready lazy component.
loading...
初回レンダリング結果は予想通りです。その後の更新プロセス(コンポーネントロード完了後 loading を実際のコンテンツに置き換える)については、むしろ Lazy コンポーネントのレンダリングメカニズムの範疇に属し、Suspense とはあまり関係がありません。ここでは展開しません。興味がある場合は React Suspense | 具体实现 を参照してください
P.S.その中で、ReadyLazyComponent の構築には少しテクニックがあります:
const ReadyLazyComponent = React.lazy(() =>
// 模拟 import('path/to/SomeOtherComponent.js')
Promise.resolve({
default: () => {
return <p>ready lazy component.</p>;
}
})
);
// 把 Lazy Component 渲染一次,触发其加载,使其 ready
const rootElement = document.getElementById("root");
// 仅用来预加载 lazy 组件,忽略缺少外层 Suspense 引发的 Warning
ReactDOM.createRoot(rootElement).render(<ReadyLazyComponent />);
setTimeout(() => {
// 等上面渲染完后,ReadyLazyComponent 就真正 ready 了
});
Lazy Component は本当に render が必要になった時にのみロードされます(いわゆる lazy)。したがって、まず 1 回レンダリングし、その後再度使用する時に ready になります
三.try...catch との類比
上記の通り、Suspense と try...catch の対応関係は:
-
Suspense:
tryに対応 -
fallback:
catchに対応 -
尚未加载完成的 Lazy Component:
Errorに対応
原理上の類似性により、Suspense の多くの特徴は try...catch との類比を通じて簡単に理解できます。例えば:
-
就近 fallback:
Errorが投げられた後、上に辿って最も近いtryに対応するcatchを探す -
存在未 ready コンポーネント就 fallback:一大块
try中、只要有一个Error就直ちにcatchに入る
したがって、Suspense で包まれた一組のコンポーネントに対して、要么全都展示出来(包括可能含有的 fallback 内容)、要么全都不展示(转而展示该 Suspense 的 fallback)。この点を理解することは Suspense を掌握するために特に重要です
パフォーマンスへの影響
前面の示例中的のように:
<Suspense fallback={<div>loading...</div>}>
<p>Hello, there.</p>
<LazyComponent />
<ReadyLazyComponent />
</Suspense>
レンダリング結果は loading... です。LazyComponent を処理する時に Suspense fallback がトリガーされるため、すでに処理完了の Hello, there. も、尚未処理到的 ReadyLazyComponent も表示できません。したがって、3 つの問題が存在します:
-
伤及池鱼:1 つの尚未加载完成的 Lazy Component 就能让它前面许多本能立即显示的组件无法显示
-
阻塞渲染:尚未加载完成的 Lazy Component 会阻断渲染流程,阻塞最近 Suspense 祖先下其后所有组件的渲染,造成串行等待
したがって、try...catch を使用するのと同じように、Suspense を濫用することも(UI レイヤーの)パフォーマンスに影響を及ぼします。技術的にはアプリケーション全体をトップレベルの Suspense に包むことで確かにすべての Lazy Component に fallback を提供できますが:
<Suspense fallback={<div>global loading...</div>}>
<App />
</Suspense>
しかし、このようにする結果を明確に意識する必要があります
構造特徴
Suspense は try...catch と同じように、固定構造を提供することで条件判断を消除します:
try {
// 如果出现 Error
} catch {
// 则进入 catch
}
分岐ロジックを構文構造に固化しました。Suspense も同様です:
<Suspense fallback={ /* 则进入 fallback */ }>
{ /* 如果出现未 ready 的 Lazy 组件 */ }
</Suspense>
このようにする利点はコード中に条件判断が現れなくて済むことです。したがって局部状態に依存せず、私たちはその作用範囲を簡単に調整できます:
<Suspense fallback={<div>loading...</div>}>
<p>Hello, there.</p>
<LazyComponent />
<ReadyLazyComponent />
</Suspense>
変更して:
<p>Hello, there.</p>
<Suspense fallback={<div>loading...</div>}>
<LazyComponent />
</Suspense>
<ReadyLazyComponent />
前後でほとんど変更コストはありません。try...catch 境界を調整するよりも容易です(変数の作用域を考慮する必要がないため)。これはloading の粒度、順序を無伤で調整するために非常に意義があります:
Suspense lets us change the granularity of our loading states and orchestrate their sequencing without invasive changes to our code.
四.オンライン Demo
文中涉及的所以重要示例,都在 Demo プロジェクト中(含详尽注释):
五.まとめ
Suspense は UI レイヤーの try...catch のようなものですが、そのキャッチするのは異常ではなく、尚未加载完成的コンポーネントです
もちろん、Error Boundary も同様で、二者は各 catch 各的互不冲突
コメントはまだありません