リアクティビティ

リアクティビティにより、Qwik はどのコンポーネントがどのステートをサブスクライブしているかを追跡できます。この情報により、Qwik はステートの変更時に該当するコンポーネントのみを無効化でき、再レンダリングが必要なコンポーネントの数を最小限に抑えることができます。

きめ細かいリアクティビティがないと、ステートの変更はルートコンポーネントからの再レンダリングが必要となり、コンポーネントツリー全体をすぐにダウンロードする必要があります。

Qwik は Angular のような変更検知を行わないことに注意することが重要です。代わりに、Qwik は、ステート全体をダーティチェックする必要なく、関連するステートが変更されたときにコンポーネントテンプレートを外科的に更新するためにシグナルに依存しています。

プロキシ

リアクティビティでは、フレームワークがアプリケーションの状態とコンポーネント間の関係を追跡する必要があります。フレームワークは、リアクティビティグラフを構築するために、少なくとも 1 回はアプリケーション全体をレンダリングする必要があります。このリアクティビティグラフの構築は、最初はサーバー側で行われ、HTML にシリアライズされるため、ブラウザはすべてのコンポーネントを 1 回パスしてグラフを再構築することを強制されることなく、この情報を使用できます。(Qwik は、イベントを登録したり、リアクティビティグラフを構築したりするためにハイドレーションを行う必要はありません)。

リアクティビティはいくつかの方法で実行できます

  1. .subscribe() を使用したリスナーの明示的な登録(例:RxJS)
  2. コンパイラーを使用した暗黙的な登録(例:Svelte)
  3. プロキシを使用した暗黙的な登録。

Qwik はいくつかの理由でプロキシを使用しています

  1. .subscribe() などの明示的な登録を使用すると、ハイドレーションを回避するために、サブスクライブされたすべてのリスナーをシリアライズする必要があります。サブスクライブされたクロージャのシリアライズは、すべてのサブスクライブ関数を遅延ロードして非同期にする必要があり(コストがかかりすぎる)、不可能になります。
  2. コンパイラーを使用してグラフを暗黙的に作成する方法は機能しますが、コンポーネントのみの場合です。コンポーネント内通信には、引き続き .subscribe() メソッドが必要となり、上記の問題が発生します。

上記の制約のため、Qwik はプロキシを使用してリアクティビティグラフを追跡します。

  • useStore() を使用して、ストアプロキシを作成します。
  • プロキシは読み取りを検出し、シリアライズ可能なサブスクリプションを作成します。
  • プロキシは書き込みを検出し、サブスクリプション情報を使用して、関連するコンポーネントを無効化します。

カウンターの例

export const Counter = component$(() => {
  const store = useStore({ count: 0 });
 
  return <button onClick$={() => store.count++}>{store.count}</button>;
});
  1. サーバーはコンポーネントの初期レンダリングを実行します。サーバーレンダリングには、store によって表されるプロキシの作成が含まれます。
  2. 初期レンダリングは OnRender メソッドを呼び出し、このメソッドは store プロキシへの参照を持ちます。レンダリングでは、プロキシが「学習」モードになります。JSX の構築中に、プロキシは count プロパティの読み取りを観察します。プロキシは「学習」モードであるため、Counter 内のテキストノードが store.count のサブスクリプションを持っていることを記録します。
  3. サーバーは、アプリケーションの状態を HTML にシリアライズします。これには、store と、Counter 内のテキストノードが store.count をサブスクライブしているというサブスクリプション情報が含まれます。
  4. ブラウザーでは、ユーザーがボタンをクリックします。クリックイベントハンドラーが store を閉じているため、Qwik はストアプロキシを復元します。プロキシには、アプリケーションの状態(カウント)と、Counter 内のテキストノードを state.count に関連付けるサブスクリプションが含まれています。
  5. イベントハンドラーは store.count をインクリメントします。store はプロキシであるため、書き込みを検出し、サブスクリプション情報を使用して、Counter 内のテキストノードを更新するためのシグナル操作を作成します。
  6. requestAnimationFrame の後、シグナル値は、テキストノードの値をシグナルの値に更新することにより、DOM に反映されます。

サブスクリプション解除の例

export const ComplexCounter = component$(() => {
  const store = useStore({ count: 0, visible: true });
 
  return (
    <>
      <button onClick$={() => (store.visible = !store.visible)}>
        {store.visible ? 'hide' : 'show'}
      </button>
      <button onClick$={() => store.count++}>increment</button>
      {store.visible ? <p>{store.count}</p> : null}
    </>
  );
});

この例は、より複雑なカウンターです。

  • これには、常に store.count をインクリメントする increment ボタンが含まれています。
  • これには、カウントを表示するかどうかを決定する show/hide ボタンが含まれています。
  1. 初期レンダリングでは、カウントが表示されます。したがって、サーバーは、store.countまたはstore.visibleのいずれかが変更された場合に、ComplexCounterを再レンダリングする必要があることを記録するサブスクリプションを作成します。
  2. ユーザーがhideをクリックすると、ComplexCounterが再レンダリングされます。再レンダリングにより、すべてのサブスクリプションがクリアされ、新しいサブスクリプションが記録されます。このとき、JSXはstore.countを読み取りません。したがって、サブスクリプションのリストにはstore.visibleのみが追加されます。
  3. ユーザーがincrementをクリックすると、store.countが更新されますが、それによってコンポーネントが再レンダリングされることはありません。これは、カウンターが表示されていないため、再レンダリングは何も操作しないため、正しい動作です。
  4. ユーザーがshowをクリックすると、コンポーネントが再レンダリングされ、今回はJSXがstore.visiblestore.countの両方を読み取ります。サブスクリプションリストが再度更新されます。
  5. 次に、incrementをクリックすると、store.countが更新されます。カウントが表示されているため、ComplexCounterstore.countをサブスクライブします。

コンポーネントがJSXの異なるブランチをレンダリングするにつれて、サブスクリプションのセットが自動的に更新されることに注目してください。プロキシの利点は、アプリケーションの実行に合わせてサブスクリプションが自動的に更新され、システムが常に無効化されたコンポーネントの最小セットを計算できることです。

深いオブジェクト

これまでの例では、ストア (useStore()) はプリミティブ値を持つ単純なオブジェクトでした。

export const MyComp = component$(() => {
  const store = useStore({
    person: { first: null, last: null },
    location: null
  });
 
  store.location = {street: 'main st'};
 
  return (
    <section>
      <p>{store.person.last}, {store.person.first}</p>
      <p>{store.location.street}</p>
    </section>
  );
})

上記の例では、Qwik は子オブジェクトの personlocation を自動的にプロキシでラップし、すべての深いプロパティに正しくサブスクリプションを作成します。

上記のラッピング動作には、1つの驚くべき副作用があります。プロキシへの書き込みと読み取りは、オブジェクトを自動的にラップするため、オブジェクトの識別子が変更されることを意味します。これは通常は問題ではありませんが、開発者が覚えておくべきことです。

export const MyComp = component$(() => {
  const store = useStore({ person: null });
  const person = { first: 'John', last: 'Smith' };
  store.person = person; // store.person auto wraps object into proxy
 
  if (store.person !== person) {
    // The consequence of auto wrapping is that the object identity changes.
    console.log('store auto-wrapped person into a proxy');
  }
});

順不同レンダリング

Qwik コンポーネントは順不同でレンダリングされます。コンポーネントは、親コンポーネントを最初にレンダリングすることを強制したり、子コンポーネントをコンポーネントのレンダリングの結果としてレンダリングしたりすることなく、レンダリングできます。これは Qwik の重要なプロパティです。これにより、Qwik アプリケーションは、状態の変化によって無効化されたコンポーネントのみを再レンダリングでき、状態の変化時にコンポーネントツリー全体を再レンダリングする必要がなくなります。

コンポーネントがレンダリングされるとき、それらは自身の props にアクセスする必要があります。親コンポーネントは props を作成します。props は、コンポーネントが親から独立してレンダリングされるためにシリアライズ可能である必要があります。

子コンポーネントの無効化

コンポーネントを再レンダリングするとき、子コンポーネントの props は同じままか、更新されます。子コンポーネントは、props が変更された場合にのみ無効になります。

export const Child = component$((props: { count: number }) => {
  return <span>{props.count}</span>;
});
 
export const MyApp = component$(() => {
  const store = useStore({ a: 0, b: 0, c: 0 });
 
  return (
    <>
      <button onClick$={() => store.a++}>a++</button>
      <button onClick$={() => store.b++}>b++</button>
      <button onClick$={() => store.c++}>c++</button>
      {JSON.stringify(store)}
 
      <Child count={store.a} />
      <Child count={store.b} />
    </>
  );
});

上記の例では、2つの <Child/> コンポーネントがあります。

  • ボタンがクリックされるたびに、3つのカウンターのいずれかがインクリメントされます。カウンターの状態が変更されると、クリックごとに MyApp コンポーネントが再レンダリングされます。
  • store.c がインクリメントされた場合、子コンポーネントはどれも再レンダリングされません。(したがって、それらのコードは遅延ロードされません)
  • store.a がインクリメントされた場合、<Child count={store.a}/> のみが再レンダリングされます。
  • store.b がインクリメントされた場合、<Child count={store.b}/> のみが再レンダリングされます。

子コンポーネントは、props が変更された場合にのみ再レンダリングされることに注意してください。これは、Qwik アプリケーションの重要なプロパティであり、状態の変化によってアプリケーションが実行する必要がある再レンダリングの量を大幅に制限します。再レンダリングが少ないとパフォーマンス上の利点がありますが、実際の利点は、アプリケーションの大部分が再レンダリングする必要がない場合はダウンロードされないことです。

貢献者

このドキュメントの改善にご協力いただいたすべての貢献者に感謝します。

  • wmertens
  • bado22
  • RATIU5
  • manucorporat
  • adamdbradley
  • fleish80
  • saikatdas0790
  • dario-piotrowicz
  • the-r3aper7
  • AnthonyPAlicea
  • mhevery
  • wtlin1228
  • mrhoodz
  • thejackshelton
  • aivarsliepa
  • zanettin