一体、反応性とは!?

DDD
リリース: 2024-12-22 05:44:10
オリジナル
343 人が閲覧しました

反応性モデルの説明

序文

私がアプリケーションと 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」 (または「熱心な」反応性) と呼ばれます。このシステムは次の原則に基づいています:

  • データ ソースの初期化 (「Observables」として知られています)
  • コンポーネント/関数はこれらのデータ ソースをサブスクライブします (これらはコンシューマーです)
  • 値が変更されると、データはすぐにコンシューマー (「オブザーバー」として知られています) に伝播されます

ご想像のとおり、「PUSH」モデルは「Observable/Observer」設計パターンに依存しています。

1 番目の使用例: 初期状態と状態変化

次の初期状態を考えてみましょう。

let a = { firstName: "John", lastName: "Doe" };
const b = a.firstName;
const c = a.lastName;
const d = `${b} ${c}`;
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

WTF Is Reactivity !?

リアクティブ ライブラリ (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」システムが変更を検出し、自動的に消費者にブロードキャストします。上記の初期状態に基づいて、発生する可能性のある操作を以下に説明します。

  • データ ソース A で状態変化が発生しました!
  • A の値は B に伝播されます (データ ソース B の計算);
  • 次に、B の値が D に伝播されます (データ ソース D の計算);
  • A の値は C に伝播されます (データ ソース C の計算);
  • 最後に、C の値が D に伝播されます (データ ソース D の再計算);

WTF Is Reactivity !?

このシステムの課題の 1 つは計算の順序にあります。実際、私たちのユースケースに基づくと、D は 2 回評価される可能性があることがわかります。1 回目は前の状態の C の値で、2 回目は前の状態の C の値で評価されます。 2 回目は C の値が最新です。この種の反応性モデルでは、この課題は 「ダイヤモンド問題」 ♦️.

と呼ばれます。

2 番目の使用例: 次の反復

次に、状態が 2 つの主要なデータ ソースに依存していると仮定します。

let a = { firstName: "John", lastName: "Doe" };
const b = a.firstName;
const c = a.lastName;
const d = `${b} ${c}`;
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

E を更新するとき、システムは状態全体を再計算します。これにより、前の状態を上書きすることで、信頼できる単一の情報源を保持できます。

  • データ ソース E で状態変化が発生しました!
  • A の値は B に伝播されます (データ ソース B の計算);
  • 次に、B の値が D に伝播されます (データ ソース D の計算);
  • A の値は C に伝播されます (データ ソース C の計算);
  • E の値は C に伝播されます (データ ソース C の再計算)。
  • 最後に、C の値が D に伝播されます (データ ソース D の再計算);

WTF Is Reactivity !?

再び「ダイヤモンド問題」が発生します...今回はデータ ソース C 上で 2 回評価される可能性があり、常に D 上で評価されます。

ダイヤモンドの問題

「ダイヤモンド問題」は、「熱心な」反応性モデルにおける新しい課題ではありません。一部の計算アルゴリズム (特に MobX で使用されるアルゴリズム) は、状態計算を平準化するために「リアクティブな依存関係ツリーのノード」にタグを付けることができます。このアプローチでは、システムは最初に「ルート」データ ソース (この例では A と E)、次に B と C、最後に D を評価します。状態計算の順序を変更すると、この種の問題の解決に役立ちます。

WTF Is Reactivity !?

PULL、別名「怠惰な」反応性

2 番目の反応性モデルは 「PULL」 と呼ばれます。 "PUSH" モデルとは異なり、次の原則に基づいています:

  • リアクティブ変数の宣言
  • システムは状態の計算を延期します
  • 派生状態は依存関係に基づいて計算されます
  • システムは過剰なアップデートを回避します

覚えておくべき最も重要なのはこの最後のルールです。前のシステムとは異なり、この最後のルールは、同じデータ ソースの複数の評価を避けるために状態の計算を延期します。

1 番目の使用例: 初期状態と状態変化

前の初期状態を保持しましょう...

WTF Is Reactivity !?

この種のシステムでは、初期状態の構文は次の形式になります。

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」モデルでは、(コンポーネントから) エフェクト() を使用してリアクティブ変数 (依存関係として指定) の値を記録すると、状態変化の計算がトリガーされます。

  • D は、その依存関係 (B および C) が更新されたかどうかを確認します。
  • B は、その依存関係 (A) が更新されたかどうかを確認します。
  • A はその値を B に伝播します (B の値を計算します);
  • C は、その依存関係 (A) が更新されたかどうかを確認します。
  • A はその値を C に伝播します (C の値を計算します)
  • B と C はそれぞれの値を D に伝播します (D の値を計算します);

WTF Is Reactivity !?

依存関係をクエリするときに、このシステムの最適化が可能です。実際、上記のシナリオでは、A が更新されたかどうかを判断するために 2 回クエリが実行されます。ただし、状態が変化したかどうかを定義するには、最初のクエリで十分な場合があります。 C はこのアクションを実行する必要はありません...代わりに、A はその値をブロードキャストすることのみが可能です。

2 番目の使用例: 次の反復

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}`));
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

もう一度言いますが、システムは明示的に要求されるまで状態の計算を延期します。以前と同じ効果を使用して、新しいリアクティブ変数を更新すると、次の手順がトリガーされます:

  • D は、その依存関係 (B および C) が更新されたかどうかを確認します ;
  • B は、その依存関係 (A) が更新されたかどうかを確認します ;
  • C は、その依存関係 (A および E) が更新されたかどうかを確認します ;
  • E はその値を C に伝播し、C はメモ化 (C の値の計算) を介して A の値をフェッチします。
  • C はその値を D に伝播し、D はメモ化 (D の値の計算) を介して B の値をフェッチします。

WTF Is Reactivity !?

A の値は変更されていないため、この変数を再計算する必要はありません (同じことが B の値にも当てはまります)。このような場合、メモ化アルゴリズムを使用すると、状態計算中のパフォーマンスが向上します。

プッシュプル、別名「きめの細かい」反応性

最後の反応性モデルは、「PUSH-PULL」システムです。 「PUSH」という用語は変更通知の即時伝達を反映し、「PULL」はオンデマンドで状態値をフェッチすることを指します。このアプローチは、以下の原則に従う、いわゆる「きめの細かい」反応性と密接に関連しています。

  • リアクティブ変数の宣言 (リアクティブ プリミティブについて話しています)
  • 依存関係は原子レベルで追跡されます
  • 変更の伝播は非常に的を絞ったものです

この種の反応性は「PUSH-PULL」モデルに限定されたものではないことに注意してください。きめ細かい反応性とは、システムの依存関係を正確に追跡することを指します。したがって、この方法でも機能する PUSHPULL 反応性モデルがあります (私は Jotai または Recoil について考えています。

1 番目の使用例: 初期状態と状態変化

まだ前の初期状態に基づいています...「きめの細かい」反応性システムにおける初期状態の宣言は次のようになります:

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 を評価します。これは直感的な操作モードではないでしょうか?

WTF Is Reactivity !?

2 番目の使用例: 次の反復

次の状態を考えてみましょう。

a.next({ firstName: "Jane", lastName: "Doe" });
ログイン後にコピー
ログイン後にコピー

もう一度言いますが、PUSH-PULL システムの「きめ細かい」側面により、各状態の自動追跡が可能になります。したがって、派生状態 C はルート状態 A と E を追跡するようになりました。変数 E を更新すると、次のアクションがトリガーされます:

  • リアクティブプリミティブ E の状態変化!
  • 対象を絞った変更通知 (C を介して E から D);
  • E はその値を C に伝播し、C はメモ化 (C の値の計算) を介して A の値を取得します。
  • C はその値を D に伝播し、D はメモ化 (D の値の計算) を介して B の値を取得します。

WTF Is Reactivity !?

これは、このモデルを非常に効率的にする、リアクティブな依存関係の相互の事前の関連付けです!

実際、古典的な "PULL" システム (React の Virtual DOM など) では、コンポーネントからリアクティブ状態を更新すると、フレームワークに変更が通知されます (" 違います」フェーズ)。次に、オンデマンド (および遅延) で、フレームワークはリアクティブな依存関係ツリーを走査して変更を計算します。変数が更新されるたびに!この依存関係の状態の「発見」には多大なコストがかかります...

「きめ細かい」リアクティブ システム (シグナルなど) を使用すると、リアクティブ変数/プリミティブの更新により、それらにリンクされている派生状態に変更が自動的に通知されます。したがって、関連する依存関係を (再) 検出する必要はありません。状態の伝播がターゲットです!

結論(.value)

2024 年、ほとんどの Web フレームワークは、特に反応性モデルの観点から、その動作方法を再考することを選択しました。この変化により、企業の効率性と競争力は全般的に向上しました。他の人は (まだ) ハイブリッド (ここでは Vue について考えています) を選択しており、これにより多くの状況でより柔軟になります。

最後に、どのモデルを選択しても、私の意見では、(良い) リアクティブ システムはいくつかの主要なルールに基づいて構築されます。

  1. システムは、一貫性のない派生 状態を防止します。
  2. システム内で状態を使用すると、リアクティブに派生した状態が生成されます。
  3. システムは過剰な作業を最小限に抑えます ;
  4. そして、「与えられた初期状態について、状態がたどる経路に関係なく、システムの最終結果は常に同じになります! 「

この最後の点は、宣言型プログラミングの基本原則として解釈できますが、(良い) リアクティブ システムは決定論的である必要があると私がどのように考えているかを示しています。これは、アルゴリズムの複雑さに関係なく、リアクティブ モデルを信頼性が高く、予測可能で、大規模な技術プロジェクトで使いやすくする「決定論」です。

以上が一体、反応性とは!?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート