ステージ1(説明)
TC39 提案のチャンピオン: ダニエル・エーレンバーグ、イェフダ・カッツ、ジャティン・ラマナタン、シェイ・ルイス、クリステン・ヒューウェル・ギャレット、ドミニク・ガナウェイ、プレストン・セゴ、マイロ・M、ロブ・アイゼンバーグ
原作者: ロブ・アイゼンバーグ、ダニエル・エーレンバーグ
このドキュメントでは、ES2015 の TC39 によって標準化された Promises に先立って行われた Promises/A+ の取り組みと同様、JavaScript におけるシグナルの初期の共通方向性について説明します。ポリフィルを使用して、自分で試してみてください。
Promises/A+ と同様に、この取り組みは JavaScript エコシステムの調整に焦点を当てています。この調整が成功すれば、その経験に基づいて標準が現れる可能性があります。ここでは、数人のフレームワーク作成者が、リアクティブ コアをサポートできる共通モデルで協力しています。現在のドラフトは、Angular、Bubble、Ember、FAST、MobX、Preact、Qwik、RxJS、Solid、Starbeam、Svelte、Vue、Wiz などの作成者/管理者からの設計入力に基づいています。
Promises/A+ とは異なり、私たちは一般的な開発者向けのサーフェス API を解決しようとしているのではなく、基礎となるシグナル グラフの正確なコア セマンティクスを解決しようとしています。この提案には完全に具体的な API が含まれていますが、この API はほとんどのアプリケーション開発者を対象としていません。代わりに、ここでのシグナル API はフレームワークの上に構築するのに適しており、共通のシグナル グラフと自動追跡メカニズムを通じて相互運用性を提供します。
この提案の計画は、ステージ 1 を超えて進む前に、いくつかのフレームワークへの統合を含む重要な初期プロトタイピングを行うことです。シグナルが複数のフレームワークで実際に使用するのに適しており、フレームワークに比べて実際のメリットが得られる場合にのみ、シグナルの標準化に関心があります。信号を提供しました。私たちは、重要な初期のプロトタイピングによってこの情報が得られることを願っています。詳細については、下記の「状況と開発計画」を参照してください。
複雑なユーザー インターフェイス (UI) を開発するには、JavaScript アプリケーション開発者は、効率的な方法で状態を保存、計算、無効化、同期し、アプリケーションのビュー層にプッシュする必要があります。一般に、UI には単純な値の管理だけではなく、それ自体も計算される他の値や状態の複雑なツリーに依存する計算された状態のレンダリングも含まれます。 Signals の目標は、このようなアプリケーションの状態を管理するためのインフラストラクチャを提供し、開発者が繰り返しの詳細ではなくビジネス ロジックに集中できるようにすることです。
シグナルのような構造は、非 UI コンテキスト、特にビルド システムで不必要な再構築を回避する場合にも有用であることが個別に判明しています。
シグナルはリアクティブ プログラミングで使用され、アプリケーションで更新を管理する必要がなくなります。
状態の変更に基づいて更新するための宣言型プログラミング モデル。
反応性とは何かより。
変数counter
を指定すると、カウンターが偶数か奇数かに関係なく DOM にレンダリングしたいとします。 counter
変化するたびに、最新のパリティで DOM を更新する必要があります。 Vanilla JS では、次のようなものになる可能性があります。
let counter = 0;const setCounter = (値) => { カウンタ = 値; render();};const isEven = () => (counter & 1) == 0;const parity = () => isEven() ? "even" : "odd";const render = () => element.innerText = parity();// counter...setInterval(() => setCounter(counter + 1), 1000); への外部更新をシミュレートします。
これには多くの問題があります...
counter
セットアップはうるさく、定型文が重いです。
counter
状態はレンダリング システムと密接に関係しています。
counter
変化してもparity
変化しない場合 (例、カウンタが 2 から 4 になる)、不必要なパリティの計算と不必要なレンダリングが行われます。
counter
更新されたときに UI の別の部分をレンダリングしたい場合はどうすればよいでしょうか?
UI の別の部分がisEven
またはparity
のみに依存している場合はどうなるでしょうか?
この比較的単純なシナリオでも、すぐに多くの問題が発生します。 counter
に pub/sub を導入することで、これらを回避することができます。これにより、 counter
の追加のコンシューマーが状態変化に対する独自の反応を追加するためにサブスクライブできるようになります。
ただし、まだ次の問題が残っています。
parity
のみに依存するレンダリング関数は、代わりに実際にcounter
をサブスクライブする必要があることを「認識」する必要があります。
counter
と直接対話せずに、 isEven
またはparity
いずれかのみに基づいて UI を更新することはできません。
定型文を増やしました。何かを使用するときは常に、単に関数を呼び出したり変数を読み取ったりするだけではなく、サブスクライブしてそこで更新を行う必要があります。購読解除の管理も特に複雑です。
ここで、 pub/sub をcounter
だけでなくisEven
とparity
にも追加することで、いくつかの問題を解決できました。次に、 isEven
をcounter
に、 parity
をisEven
に、 render
をparity
にサブスクライブする必要があります。残念ながら、定型コードが爆発的に増えただけでなく、大量のサブスクリプションの記録が残されており、すべてを正しい方法で適切にクリーンアップしないとメモリ リークの惨事が発生する可能性があります。したがって、いくつかの問題は解決しましたが、まったく新しいカテゴリの問題と大量のコードが作成されました。さらに悪いことに、システム内のすべての状態に対してこのプロセス全体を実行する必要があります。
モデルとビューの UI におけるデータ バインディング抽象化は、JS や Web プラットフォームにそのようなメカニズムが組み込まれていないにもかかわらず、長い間、複数のプログラミング言語にわたる UI フレームワークの中核となってきました。 JS フレームワークとライブラリ内では、このバインディングを表現するさまざまな方法で大量の実験が行われ、状態または計算のセルを表すファーストクラスのデータ型と組み合わせた一方向データ フローの威力が経験によって示されています。他のデータから派生したもので、現在では「シグナル」と呼ばれることが多いです。この最上級のリアクティブ値のアプローチは、2010 年の Knockout によって、オープンソースの JavaScript Web フレームワークに初めて一般的に登場したようです。それ以来、多くのバリエーションと実装が作成されてきました。過去 3 ~ 4 年の間に、Signal プリミティブおよび関連するアプローチはさらに勢いを増し、ほぼすべての最新の JavaScript ライブラリまたはフレームワークが何らかの名前で同様のものを持っています。
シグナルを理解するために、以下で詳しく説明するシグナル API を使用して再考した上記の例を見てみましょう。
const counter = new Signal.State(0);const isEven = new Signal.Computed(() => (counter.get() & 1) == 0);const parity = new Signal.Computed(() => isEven .get() ? "even" : "odd");// ライブラリまたはフレームワークは、他の Signal プリミティブ宣言関数に基づいてエフェクトを定義します。 (() => void);effect(() => element.innerText = parity.get());// counter...setInterval(() => counter.set(counter.get()) への外部更新をシミュレートします。 + 1)、1000);
すぐにわかることがいくつかあります。
前の例から、 counter
変数周辺のノイズの多い定型文を削除しました。
値、計算、副作用を処理するための統合 API があります。
counter
とrender
間に循環参照の問題や逆さまの依存関係はありません。
手動でのサブスクリプションや簿記の必要はありません。
副作用のタイミング/スケジュールを制御する手段があります。
ただし、シグナルは API の表面に表示されるものよりもはるかに多くの情報を提供します。
自動依存関係追跡- 計算された信号は、それらの信号が単純な値であるか他の計算であるかに関係なく、依存している他の信号を自動的に検出します。
遅延評価- 計算は、宣言されたときに積極的に評価されず、依存関係が変更されたときにすぐに評価されません。これらは、値が明示的に要求された場合にのみ評価されます。
メモ化- 計算された信号は最後の値をキャッシュするため、依存関係に変更がない計算は、何度アクセスされても再評価する必要がありません。
各 Signal 実装には独自の自動追跡メカニズムがあり、計算された Signal を評価するときに発生したソースを追跡します。このため、異なるフレームワーク間でモデル、コンポーネント、ライブラリを共有することが困難になります。(シグナルが通常 JS フレームワークの一部として実装されていることを考慮すると) ビュー エンジンとの誤った結合が発生する傾向があります。
この提案の目標は、リアクティブ モデルをレンダリング ビューから完全に分離し、開発者が非 UI コードを書き直すことなく新しいレンダリング テクノロジに移行したり、JS で共有リアクティブ モデルを開発してさまざまなコンテキストにデプロイできるようにすることです。残念ながら、バージョン管理と重複のため、JS レベルのライブラリを介して強力なレベルの共有に達するのは現実的ではないことが判明しました。組み込みにより、より強力な共有保証が提供されます。
一般的に使用されるライブラリが組み込まれているため、配布するコードが少なくなることでパフォーマンスが多少向上する可能性は常にありますが、シグナルの実装は一般に非常に小さいため、この効果はそれほど大きくないと予想されます。
Signal 関連のデータ構造とアルゴリズムのネイティブ C++ 実装は、一定の係数により、JS で実現可能なものよりもわずかに効率的である可能性があると考えられます。ただし、ポリフィルに存在するものと比べて、アルゴリズムの変更は予想されません。ここではエンジンが魔法であることは期待されておらず、反応性アルゴリズム自体は明確に定義され、明確になります。
チャンピオン グループは、シグナルのさまざまな実装を開発し、これらを使用してパフォーマンスの可能性を調査することを期待しています。
既存の JS 言語 Signal ライブラリでは、次のようなものをトレースするのが難しい場合があります。
計算されたシグナルのチェーンにわたるコールスタック。エラーの因果関係を示します。
信号間の参照グラフ (一方が他方に依存する場合) -- メモリ使用量をデバッグするときに重要
組み込みシグナルにより、JS ランタイムと DevTools は、ブラウザーに組み込まれているのか、共有拡張機能を介しているのかに関係なく、シグナル検査、特にデバッグやパフォーマンス分析のサポートを改善できる可能性があります。要素インスペクター、パフォーマンス スナップショット、メモリ プロファイラーなどの既存のツールを更新して、情報の表示においてシグナルを特に強調表示することができます。
一般に、JavaScript にはかなり最小限の標準ライブラリしかありませんでしたが、TC39 の傾向として、JS はより「バッテリー内蔵」言語となり、高品質の組み込み機能セットが利用可能になりました。たとえば、Temporal は moment.js を置き換え、 Array.prototype.flat
やObject.groupBy
などの多くの小さな機能が多くの lodash ユースケースを置き換えています。利点としては、バンドル サイズの縮小、安定性と品質の向上、新しいプロジェクトに参加する際の学習量の減少、JS 開発者間で一般的に共通の語彙が挙げられます。
W3C およびブラウザ実装者による現在の取り組みは、HTML にネイティブ テンプレートを導入すること (DOM パーツとテンプレートのインスタンス化) を目指しています。さらに、W3C Web コンポーネント CG は、Web コンポーネントを拡張して完全な宣言型 HTML API を提供する可能性を検討しています。これら両方の目標を達成するには、最終的に HTML でリアクティブ プリミティブが必要になります。さらに、シグナルの統合による DOM の人間工学に基づいた多くの改善が想像でき、コミュニティからの要望も受けています。
この統合は、この提案自体の一部ではなく、後で行われる別の取り組みであることに注意してください。
標準化の取り組みは、ブラウザーに変更を加えなくても、「コミュニティ」レベルだけで役立つ場合があります。 Signals の取り組みでは、多くの異なるフレームワーク作成者が集まり、反応性、アルゴリズム、相互運用性の性質について深い議論が行われています。これはすでに便利であり、JS エンジンやブラウザに含めることを正当化するものではありません。シグナルは、有効なエコシステム情報交換以外に大きな利点がある場合にのみ JavaScript 標準に追加する必要があります。
既存の Signal ライブラリは、その核心部分では、互いにそれほど違いがないことがわかりました。この提案は、これらのライブラリの多くの重要な特性を実装することで、その成功をさらに発展させることを目的としています。
状態を表す Signal タイプ、つまり書き込み可能な Signal。これは他の人が読み取ることができる値です。
計算/メモ/派生信号タイプ。他に依存し、遅延計算されてキャッシュされます。
計算は遅延します。つまり、計算されたシグナルは、依存関係の 1 つが変更されたときにデフォルトで再計算されず、誰かが実際に読み取った場合にのみ実行されます。
計算は「グリッチフリー」です。これは、不必要な計算がまったく実行されないことを意味します。これは、アプリケーションが計算された信号を読み取るときに、重複を排除するために、実行するグラフの潜在的にダーティな部分のトポロジカルなソートが存在することを意味します。
計算はキャッシュされます。つまり、依存関係が最後に変更された後、依存関係が変更されていない場合、計算された信号はアクセス時に再計算されません。
計算された信号と状態信号に対してカスタム比較が可能で、それらに依存するさらに計算された信号をいつ更新する必要があるかを記録できます。
計算されたシグナルの依存関係 (またはネストされた依存関係) の 1 つが「ダーティ」になり、変化する条件への反応は、シグナルの値が古くなっている可能性があることを意味します。
この反応は、後で実行されるより重要な作業をスケジュールすることを目的としています。
エフェクトは、これらの反応とフレームワーク レベルのスケジューリングの観点から実装されます。
計算された信号は、これらの反応のいずれかの (ネストされた) 依存関係として登録されているかどうかに反応する機能が必要です。
JS フレームワークが独自のスケジュールを実行できるようにします。 Promise スタイルの組み込みの強制スケジューリングはありません。
同期反応は、フレームワーク ロジックに基づいて後の作業をスケジュールできるようにするために必要です。
書き込みは同期的で、すぐに有効になります (書き込みをバッチ処理するフレームワークを上で実行できます)。
エフェクトが「ダーティ」である可能性があるかどうかのチェックと、実際のエフェクトの実行を分離することができます (2 段階のエフェクト スケジューラを有効にします)。
依存関係の記録をトリガーせずにシグナルを読み取る機能 ( untrack
)
シグナル/反応性を使用するさまざまなコードベースの合成を可能にします。例:
追跡/反応性自体に関する限り、複数のフレームワークを一緒に使用する (モジュロ省略、以下を参照)
フレームワークに依存しないリアクティブ データ構造 (例: 再帰的リアクティブ ストア プロキシ、リアクティブ マップ、セット、配列など)
同期反応の単純な誤用を阻止/禁止します。
健全性のリスク: 不適切に使用すると「不具合」が発生する可能性があります。シグナルが設定されたときにすぐにレンダリングが行われる場合、不完全なアプリケーションの状態がエンド ユーザーに公開される可能性があります。したがって、この機能は、アプリケーション ロジックが終了した後、後で作業をインテリジェントにスケジュールするためにのみ使用する必要があります。
解決策: 同期反応コールバック内からのシグナルの読み取りと書き込みを禁止します。
untrack
やめることを阻止し、その不健全な性質をマークする
健全性リスク: 値が他のシグナルに依存するが、それらのシグナルが変化しても更新されない計算シグナルの作成が可能になります。追跡されていないアクセスによって計算結果が変更されない場合に使用する必要があります。
解決策: API の名前には「安全でない」とマークされています。
注: この提案では、健全性のリスクにもかかわらず、読み取り後の書き込みを制限することなく、信号が計算信号とエフェクト信号から読み書きされることを許可します。この決定は、フレームワークとの統合における柔軟性と互換性を維持するために行われました。
複数のフレームワークがシグナル/反応性メカニズムを実装するための強固な基盤である必要があります。
再帰的ストア プロキシ、デコレータ ベースのクラス フィールドの反応性、および.value
と[state, setState]
スタイルの API の両方の優れたベースとなるはずです。
セマンティクスは、さまざまなフレームワークによって有効になる有効なパターンを表現できます。たとえば、これらのシグナルは、すぐに反映される書き込み、またはバッチ化されて後で適用される書き込みの基礎となることが可能である必要があります。
この API を JavaScript 開発者が直接使用できると便利です。
アイデア: すべてのフックを提供しますが、可能であれば誤用された場合のエラーも含めます。
アイデア: 微妙な API をcrypto.subtle
のようなsubtle
名前空間に配置して、フレームワークの実装や開発ツールの構築などのより高度な使用法と、フレームワーク。
ただし、まったく同じ名前を文字通りシャドウイングしないことが重要です。
機能がエコシステムの概念と一致する場合は、共通の語彙を使用するのが良いでしょう。
「JS 開発者による使いやすさ」と「フレームワークへのすべてのフックの提供」の間の緊張
実装可能であり、良好なパフォーマンスで使用できること -- サーフェス API は過度のオーバーヘッドを引き起こしません
サブクラス化を有効にすると、フレームワークがプライベート フィールドを含む独自のメソッドとフィールドを追加できるようになります。これは、フレームワーク レベルでの追加の割り当ての必要性を回避するために重要です。以下の「メモリ管理」を参照してください。
可能であれば: 計算された Signal は、生きた状態を維持するより広範なグラフにリンクされている場合でも (たとえば、生きたままの状態を読み取ることによって)、将来の読み取りのために参照している生きたものが何もない場合、ガベージ コレクタブルである必要があります。
現在、ほとんどのフレームワークでは、生きたままの別のシグナル グラフへの参照、または別のシグナル グラフからの参照がある場合、計算されたシグナルを明示的に破棄する必要があることに注意してください。
エフェクトの有効期間が UI コンポーネントの有効期間に関連付けられており、エフェクトをとにかく破棄する必要がある場合、これはそれほど悪いことではありません。
これらのセマンティクスで実行するにはコストが高すぎる場合は、計算されたシグナルの明示的な破棄 (または「リンク解除」) を以下の API に追加する必要がありますが、現在はこの機能がありません。
別の関連目標: 割り当ての数を最小限に抑える。例:
書き込み可能なシグナルを作成します (2 つの別個のクロージャ + 配列を避けます)
エフェクトを実装する (単一の反応ごとにクロージャを回避する)
シグナルの変更を監視するための API では、追加の一時データ構造を作成しないようにします。
ソリューション: サブクラスで定義されたメソッドとフィールドの再利用を可能にするクラスベースの API
Signal API の最初のアイデアは以下のとおりです。これは単なる初期草案であり、時間の経過とともに変更されることが予想されることに注意してください。まずは完全な.d.ts
から全体の形状を把握し、それが何を意味するのかについて詳しく説明します。
Interface Signal<T> {// signalget() の値を取得します: T;}namespace Signal {// 読み書き可能な Signalclass State<T> は Signal<T> を実装します {// 値で始まる状態 Signal を作成しますtconstructor(t: T, options?: SignalOptions<T>);// signalget() の値を取得します: T;// 状態 Signal 値を tset(t: T) に設定します。 void;}// 他の Signalsclass に基づく式である Signal Computed<T = known> は Signal<T> を実装します {// コールバックによって返される値に評価される Signal を作成します。// コールバックはこのシグナルで呼び出されます。 this value として.constructor(cb: (this: Computed<T>) => T, options?: SignalOptions<T>);// signalget() の値を取得します: T;}// This名前空間には、// アプリケーション開発者ではなくフレームワーク作成者に残しておいたほうがよい「高度な」機能が含まれています。// `crypto.subtle`名前空間微妙に似ています {// すべての追跡を無効にしてコールバックを実行しますfunction untrack<T>(cb: ( ) => T): T;// 信号の読み取りを追跡している現在の計算信号を取得します(もしあれば)function currentComputed(): Computed | null;// 前回の評価時にこれが参照したすべてのシグナルの順序付きリストを返します。// Watcher の場合、監視しているシグナルのセットをリストします。function introspectSources(s: Computed | Watcher): (State | Computed)[];// この信号が含まれるウォッチャーと、最後に評価されたときにこの信号を読み取った計算信号を返します。// その計算信号が(再帰的に)watched.function introspectSinks(s: State | Computed): (Computed | Watcher)[];// この信号が Watcher によって監視されているという点で「ライブ」である場合、// またはによって読み取られる場合は True (再帰的に) live.function である計算された信号 hasSinks(s: State | Computed): boolean;// この要素が「リアクティブ」である場合、// に依存するため、True になります。何か別の信号。 hasSources が false の Computed は、// 常に同じ定数を返します。function hasSources(s: Computed | Watcher): boolean;class Watcher {// Watcher の (再帰的) ソースが書き込まれるとき、このコールバックを呼び出します。//最後の `watch` 呼び出し以降まだ呼び出されていない場合。// 通知中にシグナルの読み取りまたは書き込みを行うことはできません。constructor(notify: (this: Watcher) => void);// これらのシグナルをウォッチャーのセットに追加し、ウォッチャーがその // セット内のシグナル (またはその依存関係の 1 つ) が変更されたときにコールバックを通知するようにウォッチャーを設定します。// 引数なしで呼び出すことができます。 // 通知コールバックが再び呼び出されるように、「通知済み」状態をリセットします。watch(...s: Signal[]): void;// 監視セットからこれらのシグナルを削除します (たとえば、破棄)unwatch(...s: Signal[]): void;// ウォッチャーのセット内のまだダーティなソースのセット、またはダーティまたは保留中でまだ再評価されていないソースを含む計算された信号のセットを返しますgetPending(): Signal[];}// 監視されているか監視されていないことを監視するフックvar Watched: Symbol;var unwatched: Symbol;}interface SignalOptions<T> {// 古い間のカスタム比較関数そして新たな価値。デフォルト: Object.is.// シグナルは context.equals の this 値として渡されます?: (this: Signal<T>, t: T, t2: T) => boolean;// isWatched が有効になったときに呼び出されるコールバック以前は false だった場合は true[Signal.subtle.watched]?: (this: Signal<T>) => void;// isWatched が false になった場合は常にコールバックが呼び出されます (以前は false) true[Signal.subtle.unwatched]?: (this: Signal<T>) => void;}}
信号は、時間の経過とともに変化する可能性のあるデータのセルを表します。シグナルは、「状態」(手動で設定された単なる値) または「計算された」(他のシグナルに基づく式) のいずれかです。
計算された信号は、評価中に読み取られる他の信号を自動的に追跡することによって機能します。計算結果が読み取られると、以前に記録された依存関係が変更されているかどうかがチェックされ、変更されている場合はそれ自体が再評価されます。複数の計算された信号がネストされている場合、追跡のすべての属性は最も内側の信号に当てられます。
計算された信号は遅延型、つまりプルベースです。依存関係の 1 つが以前に変更された場合でも、アクセスされたときにのみ再評価されます。
計算されたシグナルに渡されるコールバックは、一般に、アクセスする他のシグナルの決定論的で副作用のない関数であるという意味で「純粋」である必要があります。同時に、コールバックが呼び出されるタイミングは決定的であるため、副作用を注意して使用できます。
シグナルには顕著なキャッシュ/メモ化機能が備わっています。状態シグナルと計算シグナルの両方が現在の値を記憶し、実際に変更された場合にのみ参照する計算シグナルの再計算をトリガーします。古い値と新しい値を繰り返し比較する必要さえありません。比較は、ソースシグナルがリセット/再評価されるときに一度行われ、シグナルメカニズムは、そのシグナルを参照しているものが新しい値に基づいて更新されていないことを追跡します。まだ価値があります。内部的には、これは通常、(Milo のブログ投稿) で説明されているように「グラフの色付け」によって表現されます。
計算されたシグナルは、その依存関係を動的に追跡します。実行されるたびに、最終的には異なるものに依存する可能性があり、その正確な依存関係セットはシグナル グラフ内で最新の状態に保たれます。これは、1 つのブランチのみで必要な依存関係があり、前の計算がもう 1 つのブランチを使用した場合、その一時的に使用されていない値を変更しても、プルされた場合でも、計算された信号が再計算されないことを意味します。
JavaScript Promises とは異なり、Signals ではすべてが同期的に実行されます。
Signal の新しい値への設定は同期的であり、これは、後でそれに依存する計算された Signal を読み取るときにすぐに反映されます。この突然変異の組み込みのバッチ処理はありません。
計算された信号の読み取りは同期的であり、その値は常に利用可能です。
以下で説明するように、Watchers のnotify
コールバックは、それをトリガーした.set()
呼び出し中に (ただし、グラフの色付けが完了した後) 同期的に実行されます。
Promises と同様に、Signal はエラー状態を表すことができます。計算された Signal のコールバックがスローされると、そのエラーは別の値と同様にキャッシュされ、Signal が読み取られるたびに再スローされます。
Signal
インスタンスは、時間の経過とともに更新が追跡される、動的に変化する値を読み取る機能を表します。また、別の計算されたシグナルからの追跡されたアクセスを通じて暗黙的にシグナルをサブスクライブする機能も暗黙的に含まれています。
ここでの API は、「signal」、「computed」、「state」などの名前の使用において、Signal ライブラリの大部分の間での非常に大まかなエコシステムのコンセンサスに一致するように設計されています。ただし、計算信号と状態信号へのアクセスは.get()
メソッドを介して行われます。これは、 .value
スタイルのアクセサーまたはsignal()
呼び出し構文を使用するすべての一般的な Signal API とは一致しません。
この API は、割り当ての数を減らし、既存のフレームワークでカスタマイズされたシグナルと同等以上のパフォーマンスを達成しながら、シグナルを JavaScript フレームワークへの埋め込みに適したものにするように設計されています。これは次のことを意味します:
状態信号は単一の書き込み可能なオブジェクトであり、同じ参照からアクセスしたり設定したりできます。 (以下の「機能の分離」セクションの影響を参照してください。)
状態信号と計算信号はどちらもサブクラス化できるように設計されており、パブリック クラス フィールドとプライベート クラス フィールド (およびその状態を使用するメソッド) を通じて追加のプロパティを追加するフレームワークの機能を容易にします。
さまざまなコールバック ( equals
、計算されたコールバックなど) は、コンテキストのthis
値として関連する Signal を使用して呼び出されるため、Signal ごとに新しいクロージャーは必要ありません。代わりに、コンテキストを信号自体の追加プロパティに保存できます。
この API によって適用されるいくつかのエラー条件は次のとおりです。
計算されたものを再帰的に読み取るとエラーになります。
Watcher のnotify
コールバックはシグナルの読み取りまたは書き込みができません
計算された Signal のコールバックがスローされた場合、依存関係の 1 つが変更されて再計算されるまで、Signal への後続のアクセスではキャッシュされたエラーが再スローされます。
強制されないいくつかの条件:
計算された信号は、コールバック内で同期的に他の信号に書き込むことができます
Watcher のnotify
コールバックによってキューに入れられた作業はシグナルの読み取りまたは書き込みを行うことができるため、シグナルに関して古典的な React アンチパターンを複製することが可能になります。
上記で定義されたWatcher
インターフェイスは、エフェクト用の典型的な JS API を実装するための基礎を提供します。コールバックは、他のシグナルが変更されたときに、純粋に副作用のために再実行されます。上の最初の例で使用したeffect
関数は、次のように定義できます。
// この関数は通常、アプリケーション コードではなく、ライブラリ/フレームワーク内に存在します。// 注: このスケジューリング ロジックは基本的すぎて役に立ちません。コピー/ペーストしないでください。let pending = false;let w = new Signal.subtle.Watcher(() => {if (!pending) {pending = true;queueMicrotask(() => {pending = false;for (let s of w.getPending()) s.get();w.watch();});}});// cb に評価されるエフェクト エフェクト Signal で、// 自体の読み取りをスケジュールします。依存関係の 1 つが変更される可能性がある場合は常にマイクロタスク キューexport function effect(cb) {let destructor;let c = new Signal.Computed(() => { destructor?.(); destructor = cb(); });w.watch (c);c.get();return () => { デストラクタ?.(); w.unwatch(c) };}
Signal API には、 effect
のような組み込み関数は含まれていません。これは、エフェクトのスケジューリングが微妙であり、多くの場合、フレームワークのレンダリング サイクルや、JS がアクセスできない他の高レベルのフレームワーク固有の状態や戦略と結びついているためです。
ここで使用されるさまざまな操作について説明します。 Watcher
コンストラクターに渡されるnotify
コールバックは、シグナルが「クリーン」状態 (キャッシュが初期化され有効であることがわかっている) から「チェック済み」または「ダーティ」状態になるときに呼び出される関数です。 " 状態 (キャッシュが再帰的に依存する状態の少なくとも 1 つが変更されたため、キャッシュが有効であるか無効である可能性があります)。
notify
の呼び出しは、最終的には、何らかの状態 Signal での.set()
の呼び出しによってトリガーされます。この呼び出しは同期的です。つまり、 .set
が返される前に行われます。ただし、 notify
コールバック中は、たとえアンuntrack
呼び出しであっても、Signal の読み取りや書き込みができないため、このコールバックが処理途中の状態で Signal グラフを観察することについて心配する必要はありません。 notify
.set()
中に呼び出されるため、完了していない可能性がある別のロジック スレッドを中断します。 notify
からシグナルを読み書きするには、後でアクセスできるようにリストにシグナルを書き込むか、上記のqueueMicrotask
を使用するなどして、後で実行する作業をスケジュールします。
Glimmer のように、計算されたシグナルのポーリングをスケジュールすることで、 Symbol.subtle.Watcher
使用せずにシグナルを効果的に使用することは完全に可能であることに注意してください。ただし、多くのフレームワークでは、このスケジューリング ロジックを同期的に実行すると非常に便利であることが判明したため、Signals API に組み込まれています。
計算信号と状態信号の両方が、他の JS 値と同様にガベージ コレクションされます。しかし、ウォッチャーには、物事を生きた状態に保持する特別な方法があります。ウォッチャーによって監視されているシグナルは、基礎となる状態のいずれかが到達可能である限り、生きたまま保持されます。これは、これらの信号が将来のnotify
呼び出し (そして将来の.get()
をトリガーする可能性があるためです。 .get()
)。このため、エフェクトをクリーンアップするには、必ずWatcher.prototype.unwatch
を呼び出してください。
Signal.subtle.untrack
は、読み取りを追跡せずにシグナルを読み取ることを可能にするエスケープ ハッチです。この機能は、値が他の信号に依存するが、それらの信号が変化しても更新されない計算信号の作成を許可するため、安全ではありません。追跡されていないアクセスによって計算結果が変更されない場合に使用する必要があります。
これらの機能は後で追加される可能性がありますが、現在のドラフトには含まれていません。これらが省略されているのは、フレームワーク間の設計空間で確立されたコンセンサスが欠如していることと、このドキュメントで説明されているシグナルの概念に基づくメカニズムを使用してそれらの欠落を回避できることが証明されているためです。ただし、残念ながら、この省略によりフレームワーク間の相互運用性の可能性が制限されてしまいます。この文書で説明されているシグナルのプロトタイプが作成されるにつれて、これらの省略が適切な決定であったかどうかを再検討する取り組みが行われる予定です。
Async : このモデルでは、信号は常に同期して評価に使用できます。ただし、シグナルの設定につながる特定の非同期プロセスがあり、シグナルがまだ「ロード」されているときを把握すると便利なことがよくあります。読み込み状態をモデル化する簡単な方法の 1 つは例外を使用することです。この手法を使用すると、計算された信号の例外キャッシュ動作がある程度合理的に構成されます。改良された技術については、問題 #30 で説明されています。
トランザクション:ビュー間の移行の場合、「From」と「」の両方の状態の両方のライブ状態を維持することがしばしば役立ちます。 「to」状態は、「トランザクションをコミットする)を交換する準備ができているまで、「to」状態はバックグラウンドでレンダリングしますが、「from」状態はインタラクティブなままです。両方の状態を同時に維持するには、信号グラフの状態を「分岐」する必要があり、複数の保留中の移行を一度にサポートすることさえ有用かもしれません。第73号の議論。
いくつかの可能な便利な方法も省略されています。
この提案は、2024年4月のTC39アジェンダに関するステージ1のアジェンダにあります。現在、「ステージ0」と考えることができます。
この提案のポリフィルが利用可能で、いくつかの基本的なテストがあります。一部のフレームワークの著者は、このシグナル実装の代わりに実験を開始しましたが、この使用法は初期段階です。
信号提案の協力者は、この提案をどのように前進させるかについて特に保守的になりたいと考えています。私たちの計画は、TC39プロセスでは必要ではなく、次の追加タスクを行うことであり、この提案が順調に進んでいることを確認することです。
ステージ2を提案する前に、次のように計画しています。
しっかりとしたテスト(例えば、さまざまなフレームワークからの合格テストやテスト262スタイルのテスト)の複数の生産グレードのポリフィル実装を開発し、パフォーマンス(徹底的な信号/フレームワークベンチマークセットで確認された)の観点から競争力のあるものを開発します。
提案された信号APIを多少代表的であると考える多数のJSフレームワークに統合し、一部の大規模なアプリケーションはこの基礎と連携します。これらのコンテキストで効率的かつ正しく機能することをテストします。
APIの可能性のある拡張の空間を確実に理解し、この提案にどの(もしあれば)を追加すべきかを結論付けました。
このセクションでは、実装するアルゴリズムの観点から、JavaScriptにさらされた各APIについて説明します。これは、プロト仕様と考えることができ、この初期のポイントに含まれて、1つの可能なセマンティクスセットを特定しながら、非常にオープンになります。
アルゴリズムのいくつかの側面:
計算された内の信号の読み取りの順序は重要であり、特定のコールバック( Watcher
が呼び出され、 equals
、 new Signal.Computed
の最初のパラメーター、およびwatched
/ unwatched
コールバック)が実行される順に観察できます。これは、計算された信号のソースを注文して保存する必要があることを意味します。
これらの4つのコールバックはすべて例外をスローする可能性があり、これらの例外は、Calling JSコードに対して予測可能な方法で伝播されます。例外は、このアルゴリズムの実行を停止したり、グラフを半処理した状態にしたりしません。 Watcherのnotify
コールバックにスローされたエラーの場合、その例外は、複数の例外がスローされた場合にAggregateErrorを使用して、それをトリガーした.set()
コールに送信されます。その他( watched
/ unwatched
を含む?)は、信号の値に保存され、読み取られたときに再洗練され、そのようなrethrowing信号は、通常の値を持つ他の人と同じように~clean~
にマークすることができます。
「監視」されていない(監視者によって観察されている)計算された信号の場合の循環性を避けるために注意が払われているため、信号グラフの他の部分から独立して収集されるゴミを収集できます。内部的には、これは常に収集される生成数のシステムで実装できます。最適化された実装には、ローカルごとのノードごとの生成番号も含まれる場合や、監視された信号のいくつかの数値の追跡を避けることもできます。
信号アルゴリズムは、特定のグローバル状態を参照する必要があります。この状態は、スレッド全体または「エージェント」のグローバルです。
computing
: .get
または.run
コール、またはnull
のために現在再評価されている最も内側のコンピューターまたは効果信号が再評価されています。最初はnull
。
frozen
:ブール波グラフを変更しないことが必要なコールバックが現在実行されているかどうかを示します。最初はfalse
。
generation
:0から始まる整数の増分は、循環を避けながら、値の電流がどのようにあるかを追跡するために使用されます。
Signal
名空間Signal
は、信号関連のクラスと関数の名前空間として機能する通常のオブジェクトです。
Signal.subtle
、同様の内側の名前空間オブジェクトです。
Signal.State
クラスSignal.State
内部スロットvalue
:状態信号の現在の値
equals
:値を変更するときに使用される比較関数
watched
:効果によって信号が観察されるときに呼び出されるコールバック
unwatched
:効果によって信号がもはや観察されなくなったときに呼び出されるコールバック
sinks
:これに依存する監視された信号のセット
Signal.State(initialValue, options)
この信号のvalue
initialValue
に設定します。
この信号をオプションにequals
設定しますか?
この信号をオプションにwatched
か?
この信号はオプションにunwatched
か?。[signal.subtle.unwatched]
この信号のsinks
空のセットに設定します
Signal.State.prototype.get()
frozen
が真実の場合は、例外を投げます。
computing
がundefined
でない場合は、この信号をcomputing
のsources
セットに追加します。
注:ウォッチャーが監視するまで、この信号のsinks
にcomputing
を追加しません。
この信号のvalue
を返します。
Signal.State.prototype.set(newValue)
現在の実行コンテキストがfrozen
場合は、例外を投げます。
この信号と値の最初のパラメーターを使用して、「信号値を設定する」アルゴリズムを実行します。
そのアルゴリズムが返された場合は~clean~
、未定義の返されます。
この信号のすべてのsinks
のstate
を(計算された信号の場合) ~dirty~
以前はきれいな場合、または(ウォッチャーの場合)〜以前は~watching~
た場合は~pending~
。
すべてのシンクの計算された信号依存関係(再帰的に)のstate
を設定して~checked~
が以前に~clean~
であった場合(つまり、汚れたマーキングを所定の位置に置いたまま)、またはウォッチャーの場合は、以前に~watching~
場合は~pending~
。
以前に~watching~
ウォッチャーは、その再帰検索で遭遇し、次に深さの順序で、
frozen
をtrueに設定します。
彼らのnotify
コールバックを呼び出します(スローされた例外を脇に保存しますが、 notify
の返品値を無視します)。
frozen
falseに復元します。
ウォッチャーのstate
を~waiting~
に設定します。
notify
Callbacksから例外がスローされた場合は、すべてのnotify
コールバックが実行された後、それを発信者に伝播します。複数の例外がある場合は、それらを一緒に集計にパッケージ化し、それを投げます。
未定義の返品。
Signal.Computed
クラスSignal.Computed
ステートマシン計算された信号のstate
は、次のいずれかです。
~clean~
:信号の値は存在し、古くしないことが知られています。
~checked~
:この信号の(間接)ソースが変更されました。この信号には値がありますが、古くなっている可能性があります。古いかどうかは、すべての即時のソースが評価された場合にのみ知られています。
~computing~
:この信号のコールバックは現在、 .get()
呼び出しの副作用として実行されています。
~dirty~
:この信号には古くなっていることが知られている値があるか、評価されたことがありません。
遷移グラフは次のとおりです。
Statediagram-V2
[*] - >汚い
ダーティ - >コンピューティング:[4]
コンピューティング - >クリーン:[5]
きれい - >汚い:[2]
クリーン - >チェック:[3]
チェック済み - >クリーン:[6]
チェック済み - >ダーティ:[1]
読み込み中移行は次のとおりです。
番号 | から | に | 状態 | アルゴリズム |
---|---|---|---|---|
1 | ~checked~ | ~dirty~ | 計算された信号であるこの信号の直接的なソースが評価されており、その値は変化しています。 | アルゴリズム:汚れた計算信号を再計算します |
2 | ~clean~ | ~dirty~ | 状態であるこの信号の直接的なソースが設定されており、以前の値と等しくない値があります。 | 方法: Signal.State.prototype.set(newValue) |
3 | ~clean~ | ~checked~ | 状態であるこの信号の再帰的ではないが即時ではないソースは、以前の値と等しい値で設定されています。 | 方法: Signal.State.prototype.set(newValue) |
4 | ~dirty~ | ~computing~ | callback を実行しようとしています。 | アルゴリズム:汚れた計算信号を再計算します |
5 | ~computing~ | ~clean~ | callback 評価を終了し、値を返したり、例外をスローしたりしました。 | アルゴリズム:汚れた計算信号を再計算します |
6 | ~checked~ | ~clean~ | この信号のすべての即時のソースは評価されており、すべてが変わらないことが発見されているため、今では古くなっていないことが知られています。 | アルゴリズム:汚れた計算信号を再計算します |
Signal.Computed
内部スロットvalue
:信号の以前のキャッシュ値、または非読み取りの計算信号の~uninitialized~
。値は、値が読み取られたときにret延する例外である場合があります。効果信号のために常にundefined
。
state
: ~clean~
、 ~checked~
、 ~computing~
、または~dirty~
。
sources
:この信号が依存する順序付けられたシグナルセット。
sinks
:この信号に依存する順序付けられた信号セット。
equals
:オプションで提供される等しいメソッド。
callback
:計算された信号の値を取得するために呼び出されるコールバック。コンストラクターに渡された最初のパラメーターに設定します。
Signal.Computed
コンストラクターコンストラクターセット
最初のパラメーターへのcallback
オプションに基づいてequals
、 Object.is
が不在の場合はデフォルトです
state
~dirty~
~uninitialized~
のvalue
Asynccontextを使用すると、Callbackがnew Signal.Computed
に渡され、コンストラクターが呼び出されたときからスナップショット上でクローズし、実行中にこのスナップショットを復元します。
Signal.Computed.prototype.get
現在の実行コンテキストがfrozen
いる場合、またはこの信号に状態~computing~
がある場合、またはこの信号が効果で計算された信号computing
場合、例外をスローします。
computing
がnull
でない場合は、この信号をcomputing
のsources
セットに追加します。
注:ウォッチャーが監視するまで/監視されない限り、この信号のsinks
セットにcomputing
を追加しません。
この信号の状態が~dirty~
または~checked~
:この信号が~clean~
:
sources
介して再審理して、マークされた~dirty~
( ~clean~
〜computed信号を押したときに検索を切断し、この計算された信号を最後のものとして含める、最も深く、左端の(つまり、最も早く観測された)再帰ソースを見つけます。検索する)。
その信号で「汚れたコンピューター信号の再計算」アルゴリズムを実行します。
この時点で、この信号の状態は~clean~
になり、再帰ソースは~dirty~
または~checked~
。信号のvalue
を返します。値が例外である場合、その例外をrethrowします。
Signal.subtle.Watcher
クラスSignal.subtle.Watcher
State Machineウォッチャーのstate
は、次のいずれかです。
~waiting~
: notify
コールバックが実行されたか、ウォッチャーが新しくなっていますが、シグナルを積極的に視聴していません。
~watching~
:ウォッチャーは積極的に信号を見ていますが、 notify
コールバックを必要とする変更はまだ発生していません。
~pending~
:ウォッチャーの依存関係が変更されましたが、 notify
コールバックはまだ実行されていません。
遷移グラフは次のとおりです。
Statediagram-V2
[*] - >待っています
待機 - >ウォッチング:[1]
ウォッチング - >待機:[2]
監視 - >保留中:[3]
保留中 - >待機:[4]
読み込み中移行は次のとおりです。
番号 | から | に | 状態 | アルゴリズム |
---|---|---|---|---|
1 | ~waiting~ | ~watching~ | ウォッチャーのwatch 方法が呼び出されました。 | 方法: Signal.subtle.Watcher.prototype.watch(...signals) |
2 | ~watching~ | ~waiting~ | ウォッチャーのunwatch メソッドが呼び出され、最後に監視された信号が削除されました。 | 方法: Signal.subtle.Watcher.prototype.unwatch(...signals) |
3 | ~watching~ | ~pending~ | 監視された信号が値を変更した可能性があります。 | 方法: Signal.State.prototype.set(newValue) |
4 | ~pending~ | ~waiting~ | notify コールバックが実行されました。 | 方法: Signal.State.prototype.set(newValue) |
Signal.subtle.Watcher
内部スロットstate
: ~watching~
、 ~pending~
または~waiting~
signals
:このウォッチャーが見ているシグナルの順序付けられたセット
notifyCallback
:何かが変更されたときに呼び出されるコールバック。コンストラクターに渡された最初のパラメーターに設定します。
new Signal.subtle.Watcher(callback)
state
~waiting~
。
空のセットとしてsignals
初期化します。
notifyCallback
は、コールバックパラメーターに設定されています。
asynccontextを使用すると、コールバックがnew Signal.subtle.Watcher
に渡された場合、コンストラクターが呼び出されたときからスナップショットを閉じていないため、書き込みに関するコンテキスト情報が表示されます。
Signal.subtle.Watcher.prototype.watch(...signals)
frozen
が真実の場合は、例外を投げます。
引数のいずれかが信号ではない場合は、例外を投げます。
このオブジェクトのsignals
の最後にすべての引数を追加します。
新しく視聴された各信号に対して、左から右への順序で、
このウォッチャーをその信号のsink
として追加します。
これが最初のシンクである場合は、その信号をシンクとして追加するためにソースに再発します。
frozen
をtrueに設定します。
存在する場合は、 watched
コールバックを呼び出します。
frozen
falseに復元します。
信号のstate
が~waiting~
場合は、 ~watching~
に設定します。
Signal.subtle.Watcher.prototype.unwatch(...signals)
frozen
が真実の場合は、例外を投げます。
引数のいずれかが信号ではない場合、またはこのウォッチャーによって監視されていない場合は、例外を投げます。
引数の各信号について、左から右への順序で、
このウォッチャーのsignals
セットからその信号を削除します。
その信号のsink
セットからこのウォッチャーを削除します。
その信号のsink
セットが空になった場合は、その信号を各ソースからシンクとして削除します。
frozen
をtrueに設定します。
存在する場合は、 unwatched
コールバックを呼び出します。
frozen
falseに復元します。
ウォッチャーにsignals
がなく、そのstate
が~watching~
場合は、 ~waiting~
に設定してください。
Signal.subtle.Watcher.prototype.getPending()
~dirty~
で計算された信号であるsignals
のサブセットを含む配列を返します~pending~
Signal.subtle.untrack(cb)
c
実行コンテキストの現在のcomputing
状態とします。
computing
nullに設定します。
cb
に電話してください。
computing
c
に復元します( cb
例外を投げた場合でも)。
cb
の返品値を返します(例外を繰り返します)。
注:UNTRACKは、厳密に維持されているfrozen
状態からあなたを追い出すことはありません。
Signal.subtle.currentComputed()
現在のcomputing
値を返します。
この信号のsources
セットをクリアし、それらのソースのsinks
セットから削除します。
以前のcomputing
値を保存し、この信号にcomputing
設定します。
この信号の状態を~computing~
に設定します。
この信号をこの値として使用して、この計算された信号のコールバックを実行します。返品値を保存し、コールバックが例外を投げた場合、それをre延のために保存してください。
以前のcomputing
値を復元します。
「Set Signal値」アルゴリズムをコールバックの返品値に適用します。
この信号の状態を~clean~
。
そのアルゴリズムが返された場合~dirty~
:この信号のすべてのシンクを~dirty~
(以前は、シンクはチェックされたものと汚れた混合物であった可能性があります)。 (または、これが巻き戻されていない場合は、新しい世代の数を採用して汚れ、またはそのようなものを示してください。)
それ以外の場合、そのアルゴリズムは~clean~
:この場合、この信号のすべての信号のシンクがすべてクリーン~checked~
た場合、その信号のすべてのシンクがクリーンになり、その信号も~clean~
をマークします。このクリーンアップステップを適用して、シンクをチェックした新しくクリーンな信号に再帰的にシンクします。 (または、これが揺るぎない場合、何らかの形で同じことを示しているため、クリーンアップが怠lazに進むことがあります。)
このアルゴリズムが値に渡された場合(再計算された汚れた計算信号アルゴリズムから、rethrowingの例外とは対照的に):
この信号をequals
呼び出し、パラメーターとして現在のvalue
、新しい値、およびこの信号をパラメーターとして渡します。例外がスローされている場合は、その例外を信号の値として保存し、コールバックがfalseを返したかのように継続します。
その関数がtrueを返した場合は、〜creats〜 ~clean~
。
この信号のvalue
パラメーターに設定します。
~dirty~
Q :2022年に彼らがちょうど熱い新しいものになり始めたときに、信号に関連するものを標準化するのはもうすぐではありませんか?進化して安定する時間を彼らに与えるべきではありませんか?
A :Webフレームワークの信号の現在の状態は、10年以上の継続的な開発の結果です。投資が上昇するにつれて、近年のように、ほとんどすべてのWebフレームワークが信号の非常に類似したコアモデルに近づいています。この提案は、Webフレームワークの多くの現在のリーダーの間で共有されたデザイン演習の結果であり、さまざまなコンテキストでドメインの専門家のグループを検証することなく、標準化に前進することはありません。
Q :レンダリングと所有権との緊密な統合を考えると、フレームワークで組み込みの信号を使用することもできますか?
A :フレームワーク固有の部分は、効果、スケジューリング、所有権/処分の分野にある傾向がありますが、この提案は解決しようとしません。プロトタイピング標準信号の最優先事項は、既存のフレームワークが互換性があり、優れたパフォーマンスで「その下」に座ることができることを検証することです。
Q :Signal APIは、アプリケーション開発者が直接使用することを意図していますか、それともフレームワークでラップすることを意味しますか?
A :このAPIはアプリケーション開発者(少なくともSignal.subtle
Namespace内にない部分)が直接使用できますが、特に人間工学に基づいているようには設計されていません。代わりに、ライブラリ/フレームワークの著者のニーズは優先事項です。ほとんどのフレームワークは、基本的なSignal.State
およびSignal.Computed
APIを、人間工学に基づいた傾斜を表すものでラップすることが期待されています。実際には、通常、巧妙な機能(Watcher、 untrack
)を管理するフレームワークを介して信号を使用することが最適です。レンダリングをDOMにスケジュールする - この提案はこれらの問題を解決しようとはしません。
Q :ウィジェットが破壊されたときにウィジェットに関連する信号を取り壊す必要がありますか?そのためのAPIは何ですか?
A :ここでの関連する分解操作は、 Signal.subtle.Watcher.prototype.unwatch
です。監視された信号のみをクリーンアップする必要があります(それらを巻き戻すことで)。
Q :信号はVDOMで動作しますか、それとも基礎となるHTML DOMで直接動作しますか?
A :はい!信号はレンダリングテクノロジーに依存しません。信号様コンストラクトを使用する既存のJavaScriptフレームワークは、VDOM(例えば、プアクト)、ネイティブDOM(例えば、固体)、および組み合わせ(例えば、VUE)と統合されます。組み込みの信号でも同じことが可能です。
Q :AngularやLitなどのクラスベースのフレームワークのコンテキストで信号を使用することは人間工学に基づいていますか? Svelteのようなコンパイラベースのフレームワークはどうですか?
A :Class PolyFill Readmeに示すように、クラスフィールドは、シンプルなアクセサーズデコレータを使用して信号ベースにすることができます。信号はSvelte 5のルーンに非常に密接に整合しています。コンパイラがここで定義された信号APIにルーンを変換するのは簡単です。実際、これはSvelte 5が内部で行うことです(ただし、独自の信号ライブラリを使用します)。
Q :信号はSSRで動作しますか?水分補給?繰り返し?
A :はい。 QWIKは、これらの両方のプロパティでシグナルを使用して良好な効果を発揮し、他のフレームワークには、異なるトレードオフを伴う信号を使用して、水分補給に対する他の十分に発達したアプローチがあります。状態と計算された信号を使用してQWIKの再開可能な信号をモデル化し、これをコードで証明することを計画することが可能であると考えています。
Q :信号は、Reactのように一元配置データフローで動作しますか?
A :はい、信号は一方向のデータフローのメカニズムです。信号ベースのUIフレームワークでは、モデルの関数(モデルに信号が組み込まれている場合)としての見解を表現できます。状態と計算された信号のグラフは、構造によって非環境的です。また、シグナル内で反応性アンチパッターンを再現することも可能です。たとえば、 useEffect
の内部のsetState
に相当する信号は、ウォッチャーを使用して状態信号に書き込みをスケジュールすることです。
Q :信号は、Reduxのような状態管理システムにどのように関連していますか?信号は非構造化状態を促進しますか?
A :信号は、店舗のような状態管理の抽象化の効率的な基盤を形成できます。複数のフレームワークに見られる一般的なパターンは、シグナルを使用してプロパティを内部的に表すプロキシに基づいたオブジェクトです。たとえば、Vue reactive()
、またはソリッドストアです。これらのシステムは、特定のアプリケーションの適切なレベルの抽象化での柔軟な状態グループを可能にします。
Q : Proxy
が現在処理していない信号は何ですか?
A :プロキシと信号は補完的であり、うまくいきます。プロキシを使用すると、浅いオブジェクトの操作を傍受し、信号は(セルの)依存関係グラフを調整します。信号でプロキシを支援することは、非常に人間工学に基づいたネストされた反応性構造を作る素晴らしい方法です。
この例では、 get
and set
メソッドを使用する代わりに、プロキシを使用して信号にゲッターとセッターのプロパティを持たせることができます。
const a = new Signal.state(0); const b = new Proxy(a、{ get(ターゲット、プロパティ、レシーバー){if(property === 'value'){return target.get():} } set(ターゲット、プロパティ、値、レシーバー){if(property === 'value'){ターゲット.set(value)!} }}); //仮説的な反応性コンテキストでの使用:<Template> {b.value} <button onclick = {()=> {b.value ++; }}>変更</button> </template>
細粒の反応性に最適化されたレンダラーを使用する場合、ボタンをクリックすると、 b.value
セルが更新されます。
見る:
信号とプロキシの両方で作成されたネストされた反応性構造の例:Signal-Utils
リアクティブデータ間の関係を示す事前の実装の例
議論。
Q :信号はプッシュベースですか、それともプルベースですか?
A :計算された信号の評価はプルベースです:計算された信号は、基礎となる状態がはるかに早く変更されたとしても、 .get()
が呼び出された場合にのみ評価されます。同時に、状態信号を変更すると、監視者のコールバックがすぐにトリガーされ、通知を「プッシュ」する場合があります。したがって、信号は「プッシュプル」構造と考えることができます。
Q :シグナルは、javaScriptの実行に非決定的なものを導入しますか?
A :いいえ。すべての信号操作には、明確に定義されたセマンティクスと順序があり、適合の実装間で違いはありません。より高いレベルでは、信号は特定の不変剤セットに従い、それらが「健全」である。計算された信号は常に一貫した状態で信号グラフを観察し、その実行は他の信号閉じ込めコードによって中断されません(それ自体と呼ぶものを除く)。上記の説明を参照してください。
Q :状態信号に書き込むとき、計算された信号の更新はいつスケジュールされますか?
A :スケジュールされていません!計算された信号は、次に誰かがそれを読むときにそれ自体を再計算します。同期すると、ウォッチャーのnotify
コールバックが呼び出され、フレームワークが適切だと思われる時点で読み取りをスケジュールできるようにすることができます。
Q :状態信号に書き込むのはいつですか?すぐに、またはバッチされていますか?
A :状態信号への書き込みはすぐに反映されます。次に状態信号に依存する計算された信号が読み取られる場合、コードの直後であっても、必要に応じて再計算されます。ただし、このメカニズムに固有の怠laz(計算された信号は読み取られたときにのみ計算される)は、実際には、計算がバッチで発生する可能性があることを意味します。
Q :信号が「グリッチフリー」実行を有効にすることはどういう意味ですか?
A :反応性の以前のプッシュベースのモデルは、冗長計算の問題に直面していました。状態信号の更新により、計算された信号が熱心に実行されると、最終的にこれはUIに更新をプッシュする可能性があります。しかし、次のフレームの前に発生する状態信号に別の変更がある場合、このUIへの書き込みは時期尚早かもしれません。時には、そのようなグリッチのために、不正確な中間値がエンドユーザーにさえ示されました。シグナルは、プッシュベースではなくプルベースになることにより、この動的を回避します。フレームワークがUIのレンダリングをスケジュールする時点で、適切な更新をプルし、計算と書面の両方で無駄な作業を回避します。
Q :信号が「喪失」であるとはどういう意味ですか?
A :これはグリッチフリー実行の裏側です:シグナルはデータのセルを表します。これは、時間の経過とともにデータのストリームではなく、直接的な現在の値(変更する可能性があります)を表します。したがって、他の何もせずに状態信号に2回連続して書き込む場合、最初の書き込みは「失われた」ものであり、計算された信号や効果では見られません。これは、バグではなく機能であると理解されています。
Q :ネイティブ信号は、既存のJS信号実装よりも高速になりますか?
A :私たちは(小さな一定の要因によって)そう願っていますが、これはコードで証明されていることをまだ証明しています。 JSエンジンは魔法ではなく、最終的にはJS信号の実装と同じ種類のアルゴリズムを実装する必要があります。上記のパフォーマンスについては、上記のセクションを参照してください。
Q :信号の実際的な使用に効果が必要な場合、この提案にはeffect()
関数が含まれないのはなぜですか?
A :エフェクトは本質的にスケジューリングと廃棄に結び付けられます。これらは、フレームワークによって管理され、この提案の範囲外で管理されます。代わりに、この提案には、より低いレベルのSignal.subtle.Watcher
を介して効果を実装するための基礎が含まれています。Subtle.WatcherAPI。
Q :マニュアルインターフェイスを提供するのではなく、サブスクリプションが自動化されるのはなぜですか?
A :エクスペリエンスにより、反応性の手動サブスクリプションインターフェイスは、エルゴミックであり、エラーが発生しやすいことが示されています。自動追跡はより構成可能であり、信号のコア機能です。
Q : Watcher
のコールバックは、マイクロタスクでスケジュールされるのではなく、同期して実行されるのはなぜですか?
A :コールバックは信号を読み取りまたは書き込むことができないため、同期的に呼び出すことでもたらされる不健全さはありません。典型的なコールバックは、後で読み取る配列に信号を追加するか、どこかに少しマークします。これらの種類のすべてのアクションのために別のマイクロタスクを作成することは不要であり、不可欠なほど高価です。
Q :このAPIには、私のお気に入りのフレームワークが提供するいくつかの素晴らしいものが欠けているため、信号でプログラムしやすくなります。それも標準に追加できますか?
A :多分。さまざまな拡張機能がまだ検討中です。重要であると思われる不足している機能について議論を提起するために問題を提出してください。
Q :このAPIのサイズや複雑さを減らすことはできますか?
A :このAPIを最小限に抑えることは間違いなく目標であり、上記のことでそうしようとしました。削除できるもののアイデアがある場合は、議論するために問題を提出してください。
Q :この領域で、観測可能性などのより原始的な概念で標準化作業を開始すべきではありませんか?
A :観測可能性はいくつかのことにとって良い考えかもしれませんが、信号が解決することを目的とする問題を解決しません。上記のように、オブザーバブルまたはその他のパブリッシュ/サブスクライブメカニズムは、開発者にとってエラーが発生しやすい作業が多すぎるため、多くのタイプのUIプログラミングの完全な解決策ではなく、怠inessの不足などの問題が原因で作業を無駄にします。
Q :ほとんどのアプリケーションがWebベースであるため、DOMではなくTC39でシグナルが提案されるのはなぜですか?
A :この提案の一部の共著者は、目標としてWEB以外のUI環境に関心がありますが、最近では、WebAPIがWebの外でより頻繁に実装されているため、どちらの会場にも適している可能性があります。最終的に、信号はDOM APIに依存する必要はないため、どちらの方法でも機能します。誰かがこのグループを切り替える強い理由がある場合は、問題でお知らせください。今のところ、すべての貢献者がTC39知的財産契約に署名しており、これをTC39に提示する計画です。
Q :標準信号を使用できるまで、どのくらい時間がかかりますか?
A :ポリフィルはすでに利用可能ですが、レビュープロセス中にこのAPIが進化するため、安定性に依存しないことが最善です。数か月または1年で、高品質の高性能安定したポリフィルが使用可能であるはずですが、これは依然として委員会の改訂の対象であり、まだ標準ではありません。 TC39の提案の典型的な軌跡に続いて、ポリフィルが必要ないように、いくつかのバージョンに戻るすべてのブラウザでネイティブに利用できる信号を絶対に最小限に抑えることが少なくとも2〜3年かかると予想されます。
Q :{{js/web機能が嫌いだ}}のように、間違った種類の信号を早く標準化するのを防ぐにはどうすればよいですか?
A :この提案の著者は、TC39で段階の進歩を要求する前に、プロトタイピングと物事を証明することでさらに努力します。上記の「ステータスと開発計画」を参照してください。この計画のギャップや改善の機会がある場合は、説明する問題を提出してください。