シグナル
シグナルとは?
“シグナルとは?”へのパーマリンクシグナルは、観測可能な状態を管理するためのデータ構造です。
シグナルは、単一の値、または他のシグナルに依存する計算された値を保持できます。シグナルは観測可能であるため、変更時にコンシューマに通知できます。依存グラフを形成するため、計算されたシグナルは、依存関係が変更されると再計算され、コンシューマに通知されます。
シグナルは、多くの異なるコンポーネントがアクセスおよび/または変更する可能性のある**共有観測可能な状態**をモデル化および管理するために非常に役立ちます。シグナルが更新されると、そのシグナルを使用および監視している、またはそれに依存するシグナルを使用しているすべてのコンポーネントが更新されます。
シグナルは一般的な概念であり、JavaScriptライブラリやフレームワークには多くの異なる実装とバリエーションがあります。TC39提案により、JavaScriptの一部としてシグナルを標準化しようとしています。
シグナルAPIには、通常、3つの主要な概念があります。
- 単一の値を保持する状態シグナル
- 他のシグナルに依存する可能性のある計算をラップする計算シグナル
- シグナル値が変更されたときに副作用のあるコードを実行するウォッチャまたはエフェクト
これが、提案されている標準JavaScriptシグナルAPIを使用したシグナルの例です。
//
// Code developers might write to build their signals-based state...
//
// State signals hold values:
const count = new Signal.State(0);
// Computed signals wrap computations that use other signals:
const doubleCount = new Signal.Computed(() => count.get() * 2);
//
// Lower-level code of the sort that will typically be inside frameworks and
// signal-consuming libraries...
//
// Watchers are notified when signals that they watch change:
const watcher = new Signal.subtle.Watcher(async () => {
// Notify callbacks are not allowed to access signals synchronously
await 0;
console.log('doubleCount is', doubleCount);
// Watchers have to be re-enabled after they run:
watcher.watch();
});
watcher.watch(doubleCount);
// Computed signals are lazy, so we need to read it to run the computation and
// potentially notify watchers:
doubleCount.get();
シグナルライブラリ
“シグナルライブラリ”へのパーマリンクJavaScriptには多くのシグナル実装が組み込まれています。多くはフレームワークに緊密に統合されており、それらのフレームワーク内からのみ使用可能であり、他のコードから使用できるスタンドアロンライブラリもあります。
特定のシグナルAPIにはいくつかの違いがありますが、非常に似ています。
Preactのシグナルライブラリである@preact/signals
は、比較的高速で小規模なスタンドアロンライブラリであるため、最初のLit Labsシグナル統合パッケージをその周りに構築しました。@lit-labs/preact-signals
。
JavaScript向けシグナル提案
“JavaScript向けシグナル提案”へのパーマリンクシグナルAPIの類似性、フレームワークでリアクティビティを実装するためのシグナルの増加する使用、およびシグナルを使用するシステム間の相互運用性への欲求のため、シグナルを標準化する提案が現在、https://github.com/tc39/proposal-signalsでTC39で進行中です。
Litは、この提案の公式ポリフィルと統合するための@lit-labs/signals
パッケージを提供します。
この提案は、Webコンポーネントエコシステムにとって非常にエキサイティングです。標準を採用するすべてのライブラリとフレームワークは互換性のあるシグナルを生成するため、異なるWebコンポーネントは、相互運用可能なシグナルの消費と生成に同じライブラリを使用する必要がありません。
さらに、シグナルは、新しく既存の、幅広い状態管理システムと観測可能性ライブラリの基盤になる可能性があります。MobXやReduxのようなこれらのライブラリのそれぞれは、現在、Litライフサイクルと人間工学的に統合するために特定のアダプタを必要としています。シグナルの標準化により、最終的には1つのLitアダプタ(または、シグナルのサポートがコアLitライブラリに組み込まれた場合はアダプタがまったく不要)しか必要なくなる可能性があります。
シグナルとLit
“シグナルとLit”へのパーマリンクLitは現在、TC39 Signals Proposalとの統合のための@lit-labs/signals
と、Preact Signalsとの統合のための@lit-labs/preact-signals
という2つのシグナル統合パッケージを提供しています。
TC39 Signals Proposalは、JavaScriptシステムが収束する唯一のシグナルAPIになることが約束されているため、その使用をお勧めし、このドキュメントではその使用方法に重点を置きます。
インストール
“インストール”へのパーマリンクnpmから@lit-labs/signals
をインストールします。
npm i @lit-labs/signals
使用方法
“使用方法”へのパーマリンク@lit-labs/signals
は、3つの主要なエクスポートを提供します。
- シグナルを使用するすべてのクラスに適用する
SignalWatcher
ミックスイン - 個々のシグナルをピンポイントで更新するために監視する
watch()
テンプレートディレクティブ - 監視ディレクティブをテンプレートバインディングに自動的に適用する
html
テンプレートタグ
これらを次のようにインポートします。
import {SignalWatcher, watch, signal} from '@lit-labs/signals';
@lit-labs/signals
は、便宜上、ポリフィルされたシグナルAPIの一部と、開発者がカスタムテンプレートタグを必要とする場合に、シグナル監視機能を簡単に追加できるwithWatch()
テンプレートタグファクトリもエクスポートします。
SignalWatcherによる自動監視
“SignalWatcherによる自動監視”へのパーマリンクシグナルを使用する最も簡単な方法は、カスタムエレメントクラスを定義するときにSignalWatcher
ミックスインを適用することです。ミックスインを適用すると、Litライフサイクルメソッド(render()
など)でシグナルを読み取ることができます。それらのシグナルの値の変更は、自動的に更新を開始します。イベントハンドラーなど、意味のある場所にシグナルを記述できます。
この例では、SharedCounterComponent
は共有シグナルを読み書きします。コンポーネントのすべてのインスタンスは同じ値を表示し、値が変更されるとすべて更新されます。
import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';
import {SignalWatcher, signal} from '@lit-labs/signals';
const count = signal(0);
@customElement('shared-counter')
export class SharedCounterComponent extends SignalWatcher(LitElement) {
static styles = css`
:host {
display: block;
}
`;
render() {
return html`
<p>The count is ${count.get()}</p>
<button @click=${this.#onClick}>Increment</button>
`;
}
#onClick() {
count.set(count.get() + 1);
}
}
<!-- Both of these elements will show the same counter value -->
<shared-counter></shared-counter>
<shared-counter></shared-counter>
watch()
によるピンポイント更新
“watch()
によるピンポイント更新”へのパーマリンク シグナルを使用して、コンポーネント全体ではなく、個々のバインディングをターゲットとする「ピンポイント」DOM更新を実現することもできます。これを行うには、watch()
ディレクティブを使用して個々のシグナルを監視する必要があります。
調整のために、watch()
ディレクティブによってトリガーされる更新はバッチ処理され、Litリアクティブ更新ライフサイクルにも参加します。ただし、特定のLit更新が純粋にwatch()
ディレクティブによってトリガーされた場合、更新されるバインディングは変更されたシグナルを持つバインディングのみであり、テンプレート内の残りのバインディングはスキップされます。
この例は前と同じですが、count
シグナルが変更された場合、${watch(count)}
バインディングのみが更新されます。
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';
import {SignalWatcher, watch, signal} from '@lit-labs/signals';
const count = signal(0);
@customElement('shared-counter')
export class SharedCounterComponent extends SignalWatcher(LitElement) {
static styles = css`
:host {
display: block;
}
`;
render() {
return html`
<p>The count is ${watch(count)}</p>
<button @click=${this.#onClick}>Increment</button>
`;
}
#onClick() {
count.set(count.get() + 1);
}
}
このピンポイント更新によって回避される作業は実際にはごくわずかです。スキップされるのは、render()
によって返されるテンプレートの同一性チェックと@click
バインディングの値チェックだけであり、どちらも安価です。
実際、ほとんどの場合、watch()
は「プレーン」Litテンプレートレンダリングよりも大幅なパフォーマンス向上にはなりません。これは、Litがすでに値が変更されたバインディングのDOMのみを更新するためです。
watch()
のパフォーマンス向上は、テンプレートロジックの量と更新時にスキップできるバインディングの数とともにスケールする傾向があるため、ロジックとバインディングの多いテンプレートでは、より大きな節約になります。
@lit-labs/signals
には、まだシグナル対応のrepeat()
ディレクティブが含まれていません。それまでは、配列の内容の変更は完全なレンダリングを実行します。
シグナルhtml
テンプレートタグによる自動ピンポイント更新
“シグナルhtml
テンプレートタグによる自動ピンポイント更新”へのパーマリンク @lit-labs/signals
は、バインディングに渡されるシグナル値にwatch()
ディレクティブを自動的に適用する、Litのhtml
テンプレートタグの特別なバージョンもエクスポートします。
これは、watch()
ディレクティブの余分な文字や、watch()
なしで必要なsignal.get()
呼び出しを回避するために便利です。
lit
ではなく@lit-labs/signals
からhtml
をインポートすると、自動監視機能が得られます。
import {LitElement} from 'lit';
import {SignalWatcher, html, signal} from '@lit-labs/signals';
// SharedCounterComponent ...
render() {
return html`
<p>The count is ${count}</p>
<button @click=${this.#onClick}>Increment</button>
`;
}
シグナルhtml
タグは、まだlit-analyzerと連携して動作しません。アナライザは、Signal
のT
への代入として認識するため、シグナルを使用するバインディングで型エラーを報告します。
適切なポリフィルインストールの確保
“適切なポリフィルインストールの確保”へのパーマリンク@lit-labs/signals
にはsignal-polyfill
パッケージが依存関係として含まれているため、シグナルの使用を開始するために他のものを明示的にインストールする必要はありません。
しかし、シグナルは共有グローバルデータ構造(シグナル依存グラフ)に依存しているため、ポリフィルが正しくインストールされていることが非常に重要です。ページまたはアプリには、ポリフィルパッケージのコピーが1つだけ存在できます。
ポリフィルが複数コピーインストールされている場合(互換性のないバージョンやその他のnpmの不具合が原因の場合)、シグナルグラフを分割して、一部のウォッチャが一部のシグナルと機能しなくなったり、一部のシグナルが他のシグナルの依存関係として追跡されなくなる可能性があります。
これを防ぐために、npm ls
コマンドを使用して、signal-polyfill
が1つだけインストールされていることを確認してください。
npm ls signal-polyfill
行の横にdeduped
がないsignal-polyfill
が複数表示される場合は、ポリフィルが複数コピーインストールされています。
通常は、次を実行して修正できます。
npm dedupe
それでも機能しない場合は、パッケージインストール全体で単一の互換性のあるバージョンのsignal-polyfill
が得られるまで、依存関係を更新する必要がある場合があります。
欠落機能
「不足機能」へのパーマリンク@lit-labs/signals
は、完全な機能を備えていません。シグナルをLitでより実用的かつ高性能に扱うための、いくつかの構想された機能があります。
- [ ] シグナル対応の
repeat()
ディレクティブ。これにより、配列への増分更新がより効率的になります。 - [ ] ストレージにシグナルを使用する
@property()
デコレータ。これにより、リアクティブプロパティとシグナルを統一し、汎用的なシグナルユーティリティをLitリアクティブプロパティでより簡単に使用できます。 - [ ] メソッドを計算済みシグナルとしてマークするための
@computed()
デコレータ。計算済みシグナルはメモ化されるため、コストのかかる計算に役立ちます。 - [ ] メソッドをエフェクトとしてマークするための
@effect()
デコレータ。これは、個別のユーティリティを使用するよりも、エフェクトを実行するためのより人間工学的な方法です。
便利なリソース
「便利なリソース」へのパーマリンクsignal-utils
「signal-utils」へのパーマリンク signal-utils
npmパッケージには、TC39 Signals Proposalを扱うための多くのユーティリティが含まれており、
Array
、Map
、Set
、WeakMap
、WeakSet
、Object
などのシグナルを基盤とした、オブザーバブルなコレクションが含まれています。- シグナルを基盤としたフィールドを持つクラスを構築するためのデコレータ
- エフェクトとリアクション
これらのコレクションとデコレータは、プリミティブよりも複雑な値を管理する必要がある場合が多い、シグナルからオブザーバブルなデータモデルを構築するのに役立ちます。
コレクション
「コレクション」へのパーマリンクたとえば、オブザーバブルな配列を作成できます。
import {SignalArray} from 'signal-utils/array';
const numbers = new SignalArray([1, 2, 3]);
配列からの読み取り(反復処理や.length
の読み取りなど)はシグナルアクセスとして追跡され、.push()
や.pop()
からの配列の変更は、すべてのウォッチャーに通知されます。
デコレータ
「デコレータ」へのパーマリンクデコレータを使用すると、LitElement
と同様に、オブザーバブルなフィールドを持つクラスをモデル化できます。
import {signal} from 'signal-utils';
class GameState {
@signal
accessor playerOneTotal = 0;
@signal
accessor playerTwoTotal = 0;
@signal
accessor over = false;
readonly rounds = new SignalArray();
recordRound(playerOneScore, playerTwoScore) {
this.playerOneTotal += playerOneScore;
this.playerTwoTotal += playerTwoScore;
this.rounds.push([playerOneScore, playerTwoScore]);
}
}
このGameState
クラスのインスタンスは、それにアクセスするSignalWatcherクラスによって追跡され、ゲームの状態が変化すると更新されます。
ステータスとフィードバック
「ステータスとフィードバック」へのパーマリンクこのパッケージは、Lit Labsの実験的パッケージ群の一部であり、現在開発中です。不足している機能、実装における重大なバグ、コアLitライブラリよりも頻繁な破壊的変更がある可能性があります。
このパッケージは、それ自体が安定していない提案とポリフィルにも依存しています。シグナル提案の進捗に伴い、提案されたAPIに破壊的変更が行われる可能性があり、その後ポリフィルにも反映されます。
Lit統合レイヤーに関する経験を得てフィードバックを得るために、慎重な使用を推奨しますが、依存関係を注意深く管理し、慎重にテストして、予期しない破壊的変更を最小限に抑えてください。
@lit-labs/signals フィードバックディスカッションにフィードバックを残し、発生した問題を報告してください。
シグナル提案に関するフィードバックは、シグナル提案リポジトリに残すことができます。ポリフィルの問題はこちらで報告できます。