リアクティビティ
リアクティビティにより、Qwik はどのコンポーネントがどのステートをサブスクライブしているかを追跡できます。この情報により、Qwik はステートの変更時に該当するコンポーネントのみを無効化でき、再レンダリングが必要なコンポーネントの数を最小限に抑えることができます。
きめ細かいリアクティビティがないと、ステートの変更はルートコンポーネントからの再レンダリングが必要となり、コンポーネントツリー全体をすぐにダウンロードする必要があります。
Qwik は Angular のような変更検知を行わないことに注意することが重要です。代わりに、Qwik は、ステート全体をダーティチェックする必要なく、関連するステートが変更されたときにコンポーネントテンプレートを外科的に更新するためにシグナルに依存しています。
プロキシ
リアクティビティでは、フレームワークがアプリケーションの状態とコンポーネント間の関係を追跡する必要があります。フレームワークは、リアクティビティグラフを構築するために、少なくとも 1 回はアプリケーション全体をレンダリングする必要があります。このリアクティビティグラフの構築は、最初はサーバー側で行われ、HTML にシリアライズされるため、ブラウザはすべてのコンポーネントを 1 回パスしてグラフを再構築することを強制されることなく、この情報を使用できます。(Qwik は、イベントを登録したり、リアクティビティグラフを構築したりするためにハイドレーションを行う必要はありません)。
リアクティビティはいくつかの方法で実行できます
.subscribe()
を使用したリスナーの明示的な登録(例:RxJS)- コンパイラーを使用した暗黙的な登録(例:Svelte)
- プロキシを使用した暗黙的な登録。
Qwik はいくつかの理由でプロキシを使用しています
.subscribe()
などの明示的な登録を使用すると、ハイドレーションを回避するために、サブスクライブされたすべてのリスナーをシリアライズする必要があります。サブスクライブされたクロージャのシリアライズは、すべてのサブスクライブ関数を遅延ロードして非同期にする必要があり(コストがかかりすぎる)、不可能になります。- コンパイラーを使用してグラフを暗黙的に作成する方法は機能しますが、コンポーネントのみの場合です。コンポーネント内通信には、引き続き
.subscribe()
メソッドが必要となり、上記の問題が発生します。
上記の制約のため、Qwik はプロキシを使用してリアクティビティグラフを追跡します。
useStore()
を使用して、ストアプロキシを作成します。- プロキシは読み取りを検出し、シリアライズ可能なサブスクリプションを作成します。
- プロキシは書き込みを検出し、サブスクリプション情報を使用して、関連するコンポーネントを無効化します。
カウンターの例
export const Counter = component$(() => {
const store = useStore({ count: 0 });
return <button onClick$={() => store.count++}>{store.count}</button>;
});
- サーバーはコンポーネントの初期レンダリングを実行します。サーバーレンダリングには、
store
によって表されるプロキシの作成が含まれます。 - 初期レンダリングは OnRender メソッドを呼び出し、このメソッドは
store
プロキシへの参照を持ちます。レンダリングでは、プロキシが「学習」モードになります。JSX の構築中に、プロキシはcount
プロパティの読み取りを観察します。プロキシは「学習」モードであるため、Counter
内のテキストノードがstore.count
のサブスクリプションを持っていることを記録します。 - サーバーは、アプリケーションの状態を HTML にシリアライズします。これには、
store
と、Counter
内のテキストノードがstore.count
をサブスクライブしているというサブスクリプション情報が含まれます。 - ブラウザーでは、ユーザーがボタンをクリックします。クリックイベントハンドラーが
store
を閉じているため、Qwik はストアプロキシを復元します。プロキシには、アプリケーションの状態(カウント)と、Counter
内のテキストノードをstate.count
に関連付けるサブスクリプションが含まれています。 - イベントハンドラーは
store.count
をインクリメントします。store
はプロキシであるため、書き込みを検出し、サブスクリプション情報を使用して、Counter
内のテキストノードを更新するためのシグナル操作を作成します。 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
ボタンが含まれています。
- 初期レンダリングでは、カウントが表示されます。したがって、サーバーは、
store.count
またはstore.visible
のいずれかが変更された場合に、ComplexCounter
を再レンダリングする必要があることを記録するサブスクリプションを作成します。 - ユーザーが
hide
をクリックすると、ComplexCounter
が再レンダリングされます。再レンダリングにより、すべてのサブスクリプションがクリアされ、新しいサブスクリプションが記録されます。このとき、JSXはstore.count
を読み取りません。したがって、サブスクリプションのリストにはstore.visible
のみが追加されます。 - ユーザーが
increment
をクリックすると、store.count
が更新されますが、それによってコンポーネントが再レンダリングされることはありません。これは、カウンターが表示されていないため、再レンダリングは何も操作しないため、正しい動作です。 - ユーザーが
show
をクリックすると、コンポーネントが再レンダリングされ、今回はJSXがstore.visible
とstore.count
の両方を読み取ります。サブスクリプションリストが再度更新されます。 - 次に、
increment
をクリックすると、store.count
が更新されます。カウントが表示されているため、ComplexCounter
はstore.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 は子オブジェクトの person
と location
を自動的にプロキシでラップし、すべての深いプロパティに正しくサブスクリプションを作成します。
上記のラッピング動作には、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 アプリケーションの重要なプロパティであり、状態の変化によってアプリケーションが実行する必要がある再レンダリングの量を大幅に制限します。再レンダリングが少ないとパフォーマンス上の利点がありますが、実際の利点は、アプリケーションの大部分が再レンダリングする必要がない場合はダウンロードされないことです。