反応性モデルの説明
私がアプリケーションと Web サイトの開発を始めてから (すでに) 10 年になりますが、JavaScript エコシステムが今日ほどエキサイティングなものはありません!
2022 年、コミュニティは「Signal」のコンセプトに魅了され、ほとんどの JavaScript フレームワークがそれを独自のエンジンに統合しました。私は Preact について考えています。Preact は 2022 年 9 月以来、コンポーネントのライフサイクルから切り離されたリアクティブ変数を提供しています。最近では、2023 年 5 月に実験的に Signals を実装した Angular が、バージョン 18 から正式に開始されました。他の JavaScript ライブラリもアプローチを再考することを選択しました...
2023 年から現在まで、私はさまざまなプロジェクトで一貫して Signals を使用してきました。その実装と使用のシンプルさは私を完全に納得させ、技術ワークショップ、トレーニング セッション、カンファレンス中にそのメリットを専門家ネットワークと共有しました。
しかし最近、このコンセプトは本当に「革命的」なのか、Signals に代わるものはあるのか、と自問し始めました。そこで、私はこの考察をさらに深く掘り下げ、リアクティブ システムへのさまざまなアプローチを発見しました。
この投稿は、さまざまな反応性モデルの概要と、それらがどのように機能するかについての私の理解を示しています。
注意: この時点では、おそらくお察しのとおり、Java の「Reactive Streams」については説明しません。そうでなければ、この投稿のタイトルを「WTF Is Backpressure!?」 ?
にしていたでしょう。反応性モデルについて話すときは、(何よりもまず) "反応性プログラミング" について話しますが、特に "反応性" について話します。
リアクティブ プログラミングは、データ ソースの変更を消費者に自動的に伝達できる開発パラダイムです。
したがって、反応性を、データの変更に応じてリアルタイムで依存関係を更新する機能として定義できます。
NB: つまり、ユーザーがフォームに入力したり送信したりすると、これらの変更に反応し、読み込みコンポーネント、または何かが起こっていることを示すその他の表示を行う必要があります。 .. 別の例として、データを非同期で受信する場合、このデータのすべてまたは一部を表示したり、新しいアクションを実行したりするなどの対応が必要です。
これに関連して、リアクティブ ライブラリは自動的に更新され、効率的に伝播する変数を提供するため、シンプルで最適化されたコードを簡単に作成できます。
効率を高めるために、これらのシステムは、値が変更された場合に限り、これらの変数を再計算/再評価する必要があります。同様に、ブロードキャストされたデータの一貫性と最新性を確保するために、システムは中間状態 (特に状態変化の計算中) を表示しないようにする必要があります。
NB: 状態とは、プログラム/アプリケーションの存続期間全体を通じて使用されるデータ/値を指します。
わかりました、しかしそれでは…これらの "反応性モデル" とは一体何なのでしょうか?
最初の反応性モデルは、「PUSH」 (または「熱心な」反応性) と呼ばれます。このシステムは次の原則に基づいています:
ご想像のとおり、「PUSH」モデルは「Observable/Observer」設計パターンに依存しています。
次の初期状態を考えてみましょう。
let a = { firstName: "John", lastName: "Doe" }; const b = a.firstName; const c = a.lastName; const d = `${b} ${c}`;
リアクティブ ライブラリ (RxJS など) を使用すると、この初期状態は次のようになります。
let a = observable.of({ firstName: "John", lastName: "Doe" }); const b = a.pipe(map((a) => a.firstName)); const c = a.pipe(map((a) => a.lastName)); const d = merge(b, c).pipe(reduce((b, c) => `${b} ${c}`));
注意: この投稿では、すべてのコード スニペットを「疑似コード」と見なす必要があります。
ここで、コンシューマ (コンポーネントなど) が、このデータ ソースが更新されるたびに状態 D の値をログに記録したいと仮定します。
d.subscribe((value) => console.log(value));
私たちのコンポーネントはデータ ストリームをサブスクライブします。まだ変化を引き起こす必要があります。
a.next({ firstName: "Jane", lastName: "Doe" });
そこから、「PUSH」システムが変更を検出し、自動的に消費者にブロードキャストします。上記の初期状態に基づいて、発生する可能性のある操作を以下に説明します。
このシステムの課題の 1 つは計算の順序にあります。実際、私たちのユースケースに基づくと、D は 2 回評価される可能性があることがわかります。1 回目は前の状態の C の値で、2 回目は前の状態の C の値で評価されます。 2 回目は C の値が最新です。この種の反応性モデルでは、この課題は 「ダイヤモンド問題」 ♦️.
と呼ばれます。次に、状態が 2 つの主要なデータ ソースに依存していると仮定します。
let a = { firstName: "John", lastName: "Doe" }; const b = a.firstName; const c = a.lastName; const d = `${b} ${c}`;
E を更新するとき、システムは状態全体を再計算します。これにより、前の状態を上書きすることで、信頼できる単一の情報源を保持できます。
再び「ダイヤモンド問題」が発生します...今回はデータ ソース C 上で 2 回評価される可能性があり、常に D 上で評価されます。
「ダイヤモンド問題」は、「熱心な」反応性モデルにおける新しい課題ではありません。一部の計算アルゴリズム (特に MobX で使用されるアルゴリズム) は、状態計算を平準化するために「リアクティブな依存関係ツリーのノード」にタグを付けることができます。このアプローチでは、システムは最初に「ルート」データ ソース (この例では A と E)、次に B と C、最後に D を評価します。状態計算の順序を変更すると、この種の問題の解決に役立ちます。
2 番目の反応性モデルは 「PULL」 と呼ばれます。 "PUSH" モデルとは異なり、次の原則に基づいています:
覚えておくべき最も重要なのはこの最後のルールです。前のシステムとは異なり、この最後のルールは、同じデータ ソースの複数の評価を避けるために状態の計算を延期します。
前の初期状態を保持しましょう...
この種のシステムでは、初期状態の構文は次の形式になります。
let a = observable.of({ firstName: "John", lastName: "Doe" }); const b = a.pipe(map((a) => a.firstName)); const c = a.pipe(map((a) => a.lastName)); const d = merge(b, c).pipe(reduce((b, c) => `${b} ${c}`));
注意: React 愛好家はおそらくこの構文を認識しているでしょう ?
リアクティブ変数を宣言すると、タプルが「誕生」します。一方は不変変数です。もう一方のこの変数の更新関数。残りのステートメント (この場合は B、C、D) は、それぞれの依存関係を「リッスン」するため、派生状態とみなされます。
d.subscribe((value) => console.log(value));
「遅延」システムの決定的な特徴は、変更がすぐには反映されず、明示的に要求された場合にのみ反映されることです。
let a = { firstName: "John", lastName: "Doe" }; const b = a.firstName; const c = a.lastName; const d = `${b} ${c}`;
「PULL」モデルでは、(コンポーネントから) エフェクト() を使用してリアクティブ変数 (依存関係として指定) の値を記録すると、状態変化の計算がトリガーされます。
依存関係をクエリするときに、このシステムの最適化が可能です。実際、上記のシナリオでは、A が更新されたかどうかを判断するために 2 回クエリが実行されます。ただし、状態が変化したかどうかを定義するには、最初のクエリで十分な場合があります。 C はこのアクションを実行する必要はありません...代わりに、A はその値をブロードキャストすることのみが可能です。
2 番目のリアクティブ変数「root」を追加して、状態をいくらか複雑にしてみましょう。
let a = observable.of({ firstName: "John", lastName: "Doe" }); const b = a.pipe(map((a) => a.firstName)); const c = a.pipe(map((a) => a.lastName)); const d = merge(b, c).pipe(reduce((b, c) => `${b} ${c}`));
もう一度言いますが、システムは明示的に要求されるまで状態の計算を延期します。以前と同じ効果を使用して、新しいリアクティブ変数を更新すると、次の手順がトリガーされます:
A の値は変更されていないため、この変数を再計算する必要はありません (同じことが B の値にも当てはまります)。このような場合、メモ化アルゴリズムを使用すると、状態計算中のパフォーマンスが向上します。
最後の反応性モデルは、「PUSH-PULL」システムです。 「PUSH」という用語は変更通知の即時伝達を反映し、「PULL」はオンデマンドで状態値をフェッチすることを指します。このアプローチは、以下の原則に従う、いわゆる「きめの細かい」反応性と密接に関連しています。
この種の反応性は「PUSH-PULL」モデルに限定されたものではないことに注意してください。きめ細かい反応性とは、システムの依存関係を正確に追跡することを指します。したがって、この方法でも機能する PUSH と PULL 反応性モデルがあります (私は Jotai または Recoil について考えています。
まだ前の初期状態に基づいています...「きめの細かい」反応性システムにおける初期状態の宣言は次のようになります:
let a = { firstName: "John", lastName: "Doe" }; const b = a.firstName; const c = a.lastName; const d = `${b} ${c}`;
注意: シグナル キーワードの使用は、ここでの単なる逸話ではありません ?
構文の点では、「PUSH」モデルに非常に似ていますが、注目すべき重要な違いが 1 つあります: 依存関係! 「きめ細かい」反応性システム、派生状態は使用する変数を暗黙的に追跡するため、派生状態の計算に必要な依存関係を明示的に宣言する必要はありません。この場合、B と C は A の値への変更を自動的に追跡し、D は B と C の両方への変更を追跡します。
let a = observable.of({ firstName: "John", lastName: "Doe" }); const b = a.pipe(map((a) => a.firstName)); const c = a.pipe(map((a) => a.lastName)); const d = merge(b, c).pipe(reduce((b, c) => `${b} ${c}`));
このようなシステムでは、リアクティブ変数の更新は、基本的な "PUSH" モデルよりも効率的です。これは、変更がそれに依存する派生変数に自動的に伝播されるためです (通知としてのみ、伝播されません)。値そのもの)。
d.subscribe((value) => console.log(value));
次に、オンデマンド (ロガー の例を見てみましょう) に応じて、システム内で D を使用すると、関連するルート状態 (この場合は A) の値がフェッチされ、値が計算されます。派生状態 (B と C) を計算し、最後に D を評価します。これは直感的な操作モードではないでしょうか?
次の状態を考えてみましょう。
a.next({ firstName: "Jane", lastName: "Doe" });
もう一度言いますが、PUSH-PULL システムの「きめ細かい」側面により、各状態の自動追跡が可能になります。したがって、派生状態 C はルート状態 A と E を追跡するようになりました。変数 E を更新すると、次のアクションがトリガーされます:
これは、このモデルを非常に効率的にする、リアクティブな依存関係の相互の事前の関連付けです!
実際、古典的な "PULL" システム (React の Virtual DOM など) では、コンポーネントからリアクティブ状態を更新すると、フレームワークに変更が通知されます (" 違います」フェーズ)。次に、オンデマンド (および遅延) で、フレームワークはリアクティブな依存関係ツリーを走査して変更を計算します。変数が更新されるたびに!この依存関係の状態の「発見」には多大なコストがかかります...
「きめ細かい」リアクティブ システム (シグナルなど) を使用すると、リアクティブ変数/プリミティブの更新により、それらにリンクされている派生状態に変更が自動的に通知されます。したがって、関連する依存関係を (再) 検出する必要はありません。状態の伝播がターゲットです!
2024 年、ほとんどの Web フレームワークは、特に反応性モデルの観点から、その動作方法を再考することを選択しました。この変化により、企業の効率性と競争力は全般的に向上しました。他の人は (まだ) ハイブリッド (ここでは Vue について考えています) を選択しており、これにより多くの状況でより柔軟になります。
最後に、どのモデルを選択しても、私の意見では、(良い) リアクティブ システムはいくつかの主要なルールに基づいて構築されます。
この最後の点は、宣言型プログラミングの基本原則として解釈できますが、(良い) リアクティブ システムは決定論的である必要があると私がどのように考えているかを示しています。これは、アルゴリズムの複雑さに関係なく、リアクティブ モデルを信頼性が高く、予測可能で、大規模な技術プロジェクトで使いやすくする「決定論」です。
以上が一体、反応性とは!?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。