細粒反応性の序説


反応プログラミングは何十年も存在しました、しかし、それは流行の内外のようです.JavaScriptのフロントエンドでは、それは再び数年の間に再び上昇している.これは、フレームワークを超越し、任意の開発者のための有用な科目に精通している.
しかし、それは必ずしも簡単ではありません.始めに、反応性の異なるタイプがあります.用語と命名は、しばしば異なる人々に異なるものを意味する同じ単語でオーバーロードされます.
第二に、時々魔法のように見えます.それはそうではありません、しかし、「何」を理解する前に「方法」に気を取られないことはより難しいです.これは実用的な例で教えるために挑戦し、あまりにも理論的に行くのを防ぐために慎重なバランスになります.
この記事は「方法」に集中するつもりはない.私はこのようなライブラリによって使用されるアプローチの細かい粒状反応性に最も穏やかな導入を提供しようとしますMobX , Vue , Svelte , Knockout , and Solid .

Note: This may be different than the reactivity you might be familiar with streams like RxJS. They are related and there are similarities but they are not quite the same thing.


この記事は、人々のブランドを新たに微粒子の反応性や反応性の一般的なものを目指している間、それはまだいくつかの入門コンピュータサイエンスのトピックとJavaScriptと親しみの知識を前提とする中間レベルのトピックです.私は詳細に物事を説明するために最善を尽くしますが、コメントに質問を残してお気軽に.
CodeDandboxのコードスニペットや例を投稿します.私は、これらの例とこの記事の構文がその構文を使用する力に、私の図書館固体を使用しています.しかし、すべてのライブラリで多かれ少なかれ同じです.完全にインタラクティブな環境でこれらの例を再生するリンクに従ってください.

選手
細粒度反応性はプリミティブのネットワークから構築される.プリミティブによって、私は単純な構文Promises JavaScriptの文字列や数字のような原始的な値ではなく.
各ノードのグラフとしての役割.理想的な電気回路と考えることができます.どんな変化も同時にすべてのノードに適用されます.解決されている問題は、単一の時点で同期です.これは、ユーザーインターフェイスの構築時によく働く問題スペースです.
プリミティブの種類について学びましょう.

シグナル
信号は反応系の最も主要な部分である.それらはゲッター、セッターと値から成ります.学術論文ではしばしば信号と呼ばれるが、それらは観測量、原子、主題、またはrefsとも呼ばれている.
const [count, setCount] = createSignal(0);

// read a value
console.log(count()); // 0

// set a value
setCount(5);
console.log(count()); //5
もちろん、それだけではあまり面白くありません.これらは多かれ少なかれ何かを格納できる値です.重要な詳細は、両方ともget and set 任意のコードを実行できます.これは更新を伝播することが重要です.
関数はこれを行うための主要な方法ですが、オブジェクトのgetterやproxyを通して行うことができます.
// Vue
const count = ref(0)
// read a value
console.log(count.value); // 0

// set a value
count.value = 5;
またはコンパイラの背後に隠します.
// Svelte
let count = 0;
// read a value
console.log(count); // 0

// set a value
count = 5;
彼らの心臓信号では、イベントエミッタです.しかし、重要な違いは、購読が管理される方法です.

反応
信号だけでは、犯罪、反応のパートナーなしでは非常に興味深いものではありません.反応、また、効果、autoruns、時計、または計算と呼ばれる、我々の信号を観察し、それらの値を更新するたびに再実行します.
これらは最初に実行されるラップされた関数式です、そして、我々の信号が更新するたびに.
console.log("1. Create Signal");
const [count, setCount] = createSignal(0);

console.log("2. Create Reaction");
createEffect(() => console.log("The count is", count()));

console.log("3. Set count to 5");
setCount(5);

console.log("4. Set count to 10");
setCount(10);
これは最初は魔法のように見えますが、我々のシグナルがゲッターを必要とする理由です.シグナルが実行されるたびに、ラップ関数はそれを検出して、自動的にそれを購読します.私は、この行動についてもっと説明します.
重要なことは、これらのシグナルはどんな種類のデータも運ぶことができます、そして、反応はそれで何でもすることができます.CodesAndBox例では、DOM要素をページに追加するカスタムログ関数を作成しました.我々はこれらの更新プログラムを調整することができます.
第二に、更新が同期的に発生します.我々が次の命令を記録する前に、反応はすでに実行しました.
そしてそれです.我々は細かい粒度の反応性に必要なすべての作品を持っている.信号と反応観察者と観察者実際には、これらの2つのほとんどの行動を作成します.しかし、我々が話す必要がある1つの他のコア原始です.

派生
多くの場合、我々は異なる方法でデータを表現し、複数の反応で同じ信号を使用する必要があります.我々は、我々の反応でこれを書くことができるか、ヘルパーを抽出することさえできます.
console.log("1. Create Signals");
const [firstName, setFirstName] = createSignal("John");
const [lastName, setLastName] = createSignal("Smith");
const fullName = () => {
  console.log("Creating/Updating fullName");
  return `${firstName()} ${lastName()}`
};

console.log("2. Create Reactions");
createEffect(() => console.log("My name is", fullName()));
createEffect(() => console.log("Your name is not", fullName()));

console.log("3. Set new firstName");
setFirstName("Jacob");

Note: In this example fullName is a function. This is because in order for the Signals to be read underneath the Effect we need to defer executing it until the Effect is running. If it were simply a value there would be no opportunity to track or for the Effect to re-run.


しかし、時には我々の派生値の計算コストは高価であり、我々は仕事をやり直したくありません.そのために、我々は3つの基本的なプリミティブを持ちます.これらは派生として知られていますが、memos、計算、純粋な計算とも呼ばれます.
我々が作るとき、何が起こるかを比較してくださいfullName 派生.
console.log("1. Create Signals");
const [firstName, setFirstName] = createSignal("John");
const [lastName, setLastName] = createSignal("Smith");

console.log("2. Create Derivation");
const fullName = createMemo(() => {
  console.log("Creating/Updating fullName");
  return `${firstName()} ${lastName()}`
});

console.log("3. Create Reactions");
createEffect(() => console.log("My name is", fullName()));
createEffect(() => console.log("Your name is not", fullName()));

console.log("4. Set new firstName");
setFirstName("Jacob");
今回fullName 作成時にその値を計算し、反応によって読み取られるとその式を再実行しません.我々がソース信号を更新するとき、それは再び再実行します、しかし、その変化が反応に伝播するので、一度だけ.
完全な名前を計算している間、私たちは、派生がどのように私たちが独立して実行された式で値をキャッシュすることによって私たちを動かすことができるかについて見ることができる高価な計算でありません.
より多くのように、彼らは彼らが同期していることが保証されて派生します.任意の時点で、我々は彼らの依存関係を決定することができますし、彼らは古いことができるかどうかを評価します.他のシグナルに対する反応を使用することは同等であるかもしれませんが、その保証をもたらすことができないかもしれません.それらの反応はシグナルの明示的な依存性ではない(シグナルが依存関係を持たない場合).次のセクションで依存関係の概念を見ていきます.

Note: Some libraries lazy evaluate Derivations as they only need to be calculated upon read and it allows for aggressive disposal of Derivations that are not currently being read. There are tradeoffs between these approaches that go beyond the scope of this article.



反応性ライフサイクル

細粒反応性は多くの反応性ノード間の接続を維持する.グラフの任意の変更部分で再評価し、接続を作成および削除できます.

Note: Precompiled libraries like Svelte or Marko don't use the same runtime tracking technique and instead statically analyze dependencies. In so they have less control over when reactive expressions re-run so they may over-execute but there is less overhead for management of subscriptions.


どのようなデータを使用して値を取得するかを変更するときに考慮します.
console.log("1. Create");
const [firstName, setFirstName] = createSignal("John");
const [lastName, setLastName] = createSignal("Smith");
const [showFullName, setShowFullName] = createSignal(true);

const displayName = createMemo(() => {
  if (!showFullName()) return firstName();
  return `${firstName()} ${lastName()}`
});

createEffect(() => console.log("My name is", displayName()));

console.log("2. Set showFullName: false ");
setShowFullName(false);

console.log("3. Change lastName");
setLastName("Legend");

console.log("4. Set showFullName: true");
setShowFullName(true);
注目すべきことは、私たちがlastName ステップ3では、新しいログを取得しません.これは、リアクティブな式を再実行するたびに依存関係を再構築するからです.簡単に言えば、我々はlastName 誰もそれを聞いていない.
値は変更されます.showFullName 戻るにはtrue.しかし、何も通知されません.これは、2010年のために安全な相互作用ですlastName 再び追跡されるshowFullName 変更する必要がありますが追跡されます.
依存性は、反応式がその値を生成するために読み込むシグナルです.順番に、これらのシグナルは、多くの反応式の購読を保持します.彼らが更新するとき、彼らは彼らに依存する彼らの加入者に通知します.
これらのサブスクリプション/依存関係を各実行時に構築します.リアクティブエクスプレッションが再実行されるたびに、または最終的に解放されるたびにそれらを解放します.を使用してそのタイミングを見ることができますonCleanup ヘルパー
console.log("1. Create");
const [firstName, setFirstName] = createSignal("John");
const [lastName, setLastName] = createSignal("Smith");
const [showFullName, setShowFullName] = createSignal(true);

const displayName = createMemo(() => {
  console.log("### executing displayName");
  onCleanup(() =>
    console.log("### releasing displayName dependencies")
  );
  if (!showFullName()) return firstName();
  return `${firstName()} ${lastName()}`
});

createEffect(() => console.log("My name is", displayName()));

console.log("2. Set showFullName: false ");
setShowFullName(false);

console.log("3. Change lastName");
setLastName("Legend");

console.log("4. Set showFullName: true");
setShowFullName(true);

同期実行
細粒度反応系は、同期して直ちにそれらの変化を実行する.彼らは矛盾した状態を観察することは決して不可能であるという点でグリッチフリーであることを目指している.これは、任意の指定された変更コードで一度だけ実行されるため、予測可能性につながる.
矛盾した状態は、我々が決定をして、操作を行うために我々が観察するものを信頼することができないとき、意図しないふるまいに至ることができます.
どのように動作するかを示す最も簡単な方法は、2つの変更を同時に適用することです.私たちはbatch 実証するヘルパー.batch トランザクションの更新をラップします.
console.log("1. Create");
const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);
const c = createMemo(() => {
  console.log("### read c");
  return b() * 2;
});

createEffect(() => {
  console.log("### run reaction");
  console.log("The sum is", a() + c());
});

console.log("2. Apply changes");
batch(() => {
  setA(2);
  setB(3);
});
この例では、コードは期待どおりにトップダウンを生成します.しかし、バッチ更新は、実行/読み取りのログを反転します.
AとBが同時に適用されても、値を更新するときは、まず最初にどこかに出発する必要があります.したがって、効果は最初に実行されますが、Cが正常であることを検出します.
確かに、おそらくこの静的なケースを解決するためのアプローチを考えることができますが、依存関係を任意の実行で変更することを覚えておいてください.細粒反応性ライブラリは一貫性を維持するためにハイブリッドプッシュ/プル方式を用いる彼らは純粋に“イベント”/ストリームのようにプッシュされていない純粋に“発電機のようなプル”.

結論
この記事はたくさん取り上げられた.コアプリミティブを導入し,依存性解決と同期実行を含む微細粒反応性の定義特性に触れた.
トピックが完全にまだ明確に見えないならば、それはOKです.記事をレビューし、例についてのメッセージをお試しください.これらは、最も最小限の方法でアイデアを実証するためのものでした.しかし、これは本当にほとんどです.少し練習すると、あまりにも粒状の方法でデータをモデル化する方法を確認することができます.
更なる読書
The fundamental principles behind MobX
SolidJS: Reactivity to Rendering