メインコンテンツへ移動

React Suspense と try...catch

無料2019-12-07#JS#React Suspense Boundary#React Suspense原理#React Suspense机制#React Suspense教程#UI层try catch

5 行のコードで React Suspense を理解する

一.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 各的互不冲突

参考資料

コメント

コメントはまだありません

コメントを書く