3 ポイント 投稿者 GN⁺ 2024-04-01 | 1件のコメント | WhatsAppで共有
  • TC39のJavaScript Signals提案は、UIの状態と計算された状態を効率的に追跡するためのリアクティブなプリミティブを標準化しようとする初期方針であり、現在はStage 1段階のドラフト
  • この提案は、アプリケーション開発者が直接使う表面的なAPIよりも、フレームワーク間で共有できるSignalグラフの中核セマンティクスと自動追跡メカニズムに焦点を当てている
  • Signal.StateSignal.ComputedSignal.subtle.Watcherが中核APIであり、計算はlazy evaluation、キャッシュ、自動依存関係追跡、glitch-freeな実行を目指す
  • 組み込みSignalsの目的は、Angular、Ember、MobX、Preact、Qwik、Solid、Svelte、Vueなど複数のフレームワーク間の相互運用性を高め、DevToolsによるデバッグ・性能分析支援の可能性を開くこと
  • 提案グループはStage 2以前に、複数のプロダクション品質のpolyfill、フレームワーク統合、大規模アプリケーションでの検証、性能ベンチマークを経ようとしており、標準化までは少なくとも2〜3年以上かかる可能性がある

JavaScript Signals提案の位置づけ

  • JavaScript SignalsはTC39プロセスのStage 1提案として紹介されている
  • 現在の文書は、ES2015でPromiseが標準化される前に存在したPromises/A+と同様に、JavaScriptエコシステムの共通の方向性をそろえようとする試み
  • 直接試せるpolyfillが提供されている
  • 提案のチャンピオンと原著者は、複数のフレームワーク・ライブラリからの設計インプットをもとに現在のドラフトを構成している

標準化が狙う課題

  • 複雑なUIでは値を保存し、計算し、無効化し、同期し、ビュー層へプッシュする必要があり、Signalsはこうした反復作業のための状態管理インフラを提供しようとしている
  • Vanilla JSの例では、counterisEvenparityrenderが直接絡み合い、次の問題が生じる
    • 状態とレンダリングシステムが強く結合する
    • counterが2から4に変わる場合のように、parityが変わらなくても不要な計算とレンダリングが発生する
    • 他のUI断片がcounterisEvenparityの一部だけを購読しようとすると、手動の購読と解除の管理が複雑になる
    • pub/subを複数段階に追加すると、ボイラープレートと購読のbookkeepingが増え、メモリリークのリスクが生じる
  • Signalsベースの例では、Signal.StateSignal.Computedを使って、値、計算、副作用を1つの方法で扱う
    • 手動の購読は不要
    • 計算されたSignalが依存するSignalを自動的に見つける
    • 計算は値が明示的に要求されたときだけ実行される
    • 計算されたSignalは最後の値をキャッシュする

提案の中核API

  • Signal<T>get(): Tを持つ読み取り可能な値として定義される
  • Signal.State<T>は書き込み可能なSignal
    • コンストラクタは初期値とオプションを受け取る
    • get()で値を読み、set(t)で値を変更する
  • Signal.Computed<T>は他のSignalに基づく計算Signal
    • コールバックが返す値によって計算される
    • 依存関係を自動追跡する
    • 値はlazyに計算され、キャッシュされる
  • Signal.subtleは、フレームワーク作者やDevTools実装に近い高度なAPIを含む
    • untrack(cb)は追跡なしでSignalを読めるようにする
    • currentComputed()は現在追跡中のcomputed Signalを返す
    • introspectSourcesintrospectSinkshasSinkshasSourcesはグラフ観察用API
    • WatcherはSignalの変更を検知し、フレームワークレベルのeffectとスケジューリングを実装する基盤
  • SignalOptions<T>はユーザー定義の比較関数equalswatched / unwatchedフックをサポートする

動作方式と実行モデル

  • Signalは時間とともに変化し得るデータセルを表し、stateまたはcomputedに分かれる
  • 計算されたSignalは、実行中に読んだSignalを自動的に記録し、その後読まれるときに以前の依存関係が変わったか確認する
  • 計算はpull-based
    • 依存関係が変わっても即座には再計算しない
    • 誰かが.get()で読んだとき、必要なら再計算する
  • State Signalへの書き込みは同期的に反映される
    • .set()の後、その値に依存するcomputed Signalを読むと、必要に応じて即座に再計算される
    • 組み込みのbatchingはない
  • Watcherのnotifyコールバックは.set()中に同期的に実行されることがある
    • ただしnotify中はSignalを読んだり書いたりできない
    • 実際の読み書き処理はその後にスケジュールする必要がある
  • computed Signalのコールバックが例外を投げると、その例外も値のようにキャッシュされ、Signalを再度読むと再び投げられる

標準化の動機

  • 各フレームワークのSignal実装は独自の自動追跡メカニズムを持っているため、モデル・コンポーネント・ライブラリをフレームワーク間で共有しにくい
  • 提案の目標は、リアクティブモデルをレンダリングビューから分離すること
    • 開発者がレンダリング技術を変更しても、非UIコードを書き直さなくて済むようにする
    • 複数の文脈で共有できるリアクティブモデルをJavaScriptで作れるようにする
  • 性能とメモリの面では、組み込み実装がJS実装より小さな定数係数ぶん効率的である可能性はあるが、エンジンが魔法のようにアルゴリズムを変えるわけではないと明記している
  • DevToolsの面では、組み込みSignalsによって次の情報をよりよく表示できる可能性がある
    • computed Signalチェーンのcall stack
    • Signal間の参照グラフ
    • メモリ使用量のデバッグに必要な依存関係
  • 標準ライブラリに含まれる場合、バンドルサイズの削減、安定性と品質の向上、プロジェクト間での共通語彙の形成といった副次的効果も期待される

設計目標と制約

  • 中核機能には、書き込み可能なSignal、計算されたSignal、dirty状態への反応、フレームワーク独自のスケジューリング、untrack、複数コードベースの合成が含まれる
  • 計算されたSignalはglitch-freeを目標とする
    • 不要な計算を避けるため、グラフ内の潜在的にdirtyな部分をトポロジカルソートして実行する
    • 重複計算を排除しようとする
  • フレームワークが独自にスケジューリングできるよう、Promiseスタイルの組み込み強制スケジューリングは入れない
  • 同期的なリアクションコールバックの誤用を防ぐため、Watcherのnotify内ではSignalの読み書きを禁止する
  • untrackは安全でない脱出口として扱われる
    • 追跡なしで読んだSignalが計算結果に影響する場合、そのSignalの変更時にcomputed Signalが更新されない可能性がある
  • APIはフレームワーク実装の基盤になることを優先しており、一般のアプリ開発者にとって特別にergonomicに設計されたものではない

effectとWatcher

  • 提案にはeffect()のような組み込み関数はない
  • effectのスケジューリングは、フレームワークのレンダリングサイクル、disposal、所有権管理と絡み合うため、JavaScript標準APIが直接解決するものではない
  • 代わりにSignal.subtle.Watcherがeffect実装の低レベル基盤を提供する
    • watched Signalの依存関係が変更されるとnotifyが呼ばれる
    • getPending()でまだdirtyなSignalを確認できる
    • disposeが必要なeffectはunwatchで片付ける必要がある
  • Watcherが見ているSignalは内部stateが到達可能であれば生き続けることがあるため、effectの片付け時にはWatcher.prototype.unwatchの呼び出しが必要

現在のドラフトで欠けている機能

  • Asyncは現在のモデルに含まれていない
    • Signalsは常に同期的に評価可能な値として扱われる
    • loading状態を例外としてモデル化する方法は一部可能であり、改善議論はIssue #30にある
  • Transactionsも含まれていない
    • 画面遷移で「from」状態と「to」状態を同時に維持しようとすると、Signalグラフ状態をforkする問題が生じる
    • 関連する議論はIssue #73にある
  • 一部のconvenience methodsも現在のドラフトからは外れている
  • 欠けている機能は、フレームワーク間の合意不足と上位層で回避可能である点から除外されたが、プロトタイプ後に再検討される可能性がある

開発計画と標準化スケジュール

  • この提案は2024年4月のTC39 Stage 1議題に上がっており、文書上では現在Stage 0のように見られると説明している
  • Stage 2を提案する前に、次の作業を計画している
    • 複数のプロダクション品質のpolyfill実装の開発
    • さまざまなフレームワークでのテストとtest262スタイルのテスト通過
    • thorough signal/framework benchmark setによる性能検証
    • 代表的な複数のJSフレームワークと一部の大規模アプリケーションへの提案API統合
    • API拡張可能性の理解と、含めるかどうかの決定
  • 提案グループは、誤った形のSignalsが早すぎる段階で標準化されることを避けるため、保守的に進めようとしている
  • FAQでは、標準Signalsがブラウザ全般でpolyfillなしに使えるようになるまで、少なくとも2〜3年はかかると予想している
  • 現在のpolyfillは利用できるが、レビュー過程でAPIが変わる可能性があるため、安定性に依存しないほうがよいと案内している

FAQで整理された利用モデル

  • 組み込みSignalsはレンダリング技術と独立している
    • VDOMを使うPreact、native DOMを使うSolid、混合方式を使うVueのような形態はいずれも可能だと説明している
  • アプリ開発者は、おおむねフレームワークを通じてSignalsを使うのが適している
    • フレームワークがWatcher、untrack、ownership、disposal、DOMレンダリングのスケジューリングを管理する
  • SSR、hydration、resumabilityとともに利用できる
    • QwikはSignalsをこれらの特性とともに使っており、提案側はQwikのresumable SignalsをStateとComputedの組み合わせでモデル化できると見ている
  • SignalsとProxyは相互補完的
    • Proxyは浅いオブジェクト操作を横取りし、Signalsはデータセルの依存関係グラフを調整する
    • Proxyの背後にSignalsを置けば、nested reactive structureをよりergonomicにできる
  • Signalsはストリームではなく、現在値を表すセル
    • State Signalに連続して2回書き込み、何もしなければ、最初の書き込みはcomputed Signalやeffectから見えない可能性がある
    • 文書はこれをglitch-free実行の反対側にある性質と見なし、ストリームにはasync iterableやobservableのような別の構成のほうが適しているとしている

1件のコメント

 
GN⁺ 2024-04-01
Hacker News のコメント
  • 素の JavaScript の例のほうが、むしろ読みやすく扱いやすいと感じるのは自分だけだろうか
    「設定がノイズとボイラープレートだらけ」だというが、signals の例も同じくらい騒がしくボイラープレートが多く見え、初心者には理解しにくい新しい概念まで追加している
    「counter が 2 から 4 に変わると parity は変わらないのに、不要な計算とレンダリングをする」というのは、早すぎるメモ化のように聞こえる
    UI の別の部分が counter の更新に合わせてレンダリングしたいなら、その strawman の例が適切でないのは確かで、その場合は signals、イベント処理、中央状態ストア(Redux 系)など別の方法を使えばよい
    UI の別の部分が isEven や parity だけに依存しているなら、アプリの中核構造であればアプローチを変えられるかもしれないが、たいていはそうではない。「parity だけに依存する render 関数が counter を購読しなければならないという事実を知っている必要がある」というのも、必ずしも不当な負担ではなく、純粋な計算関数には入力を把握しやすいという利点がある

    • なぜこれを 早すぎるメモ化と見るのか分からない。これは単純な関数に縮約した例にすぎず、実際には必要とされたこともないのに人々がこうしたユースケースを作り出した、と考えるのは難しい
      UI 開発でますます使われるようになっている概念である signals の標準化を試みるのは称賛に値する。ボイラープレートがどの程度か、イベントシステムを自分で作る必要があるのかといった細部の議論はさておき、複数のフレームワークで signals が使われているなら理由があるのかもしれず、時間がかかっても標準化を試みる価値はある
    • 同意する。ただし Preact の signal ドキュメントを見ると、文脈がずっとよく合う
      https://preactjs.com/guide/v10/signals
      Preact では、signal が props や context としてツリーを下っていくとき、signal の参照だけを渡し、コンポーネントは値ではなく signal を見るため、signal を更新してもコンポーネントを再レンダリングしないで済む場合がある。実際に .value にアクセスしているツリー内のコンポーネントへ直接行ける
      また signal は、値がいつアクセスされ、いつ更新されるかを追跡し、Preact ではコンポーネント内で signal の .value にアクセスすると、その signal の値が変わったときにコンポーネントを自動的に再レンダリングする
    • JavaScript にはリアクティビティが組み込まれていないので、リアクティビティを追加すれば抽象化コストが生じるのは避けられない。必要なときに使うためのものであって、状態を扱う基本の方法である必要はない
      経験上の大きな利点は、リアクティブな状態をモジュール化できる点だ。命令型スタイルでは変化を追跡するために追加の状態が必要で、モジュール性は抽象化によって達成される。必要なときだけ使えばよい
      単純でありながら適用可能な例を作るのはバランスの問題だ。リアクティビティが明確に有利な場合は概してより複雑なので、単純だが適用範囲の狭い例より示しにくい
    • これを説明する方法には改善の余地がある。小さな例では問題が見えにくく、より大きな規模で現れる。PR 歓迎
    • ある複雑度の閾値を超えたときに設計変更が必要になるのは、避ける価値がある。素の JS アプローチには状態グラフの複雑さという観点でスケールの限界があり、本当の問題は閾値の前後の使い勝手ではなく、その閾値を超えた瞬間に使い勝手が不連続に変わることにある
  • JavaScript に Promises が追加されるときは、あちこちで new Promise を書かなければならないのではないかと抵抗感があった
    実際には、new Promise を直接書いた回数は両手で数えられるくらいだ。代わりに、特にサードパーティライブラリを扱うときに .then をはるかによく使うようになった
    結局 Promise が JavaScript に追加された日常的な効果は、サードパーティライブラリが提供するさまざまな特殊な動作や機能に対して、かなり単純でおおむね堅牢、ほぼ普遍的なインターフェースを与えたことだ。ファイル読み込みであれ API リクエストであれビルド段階の出力であれ、.then(res => …) を使えば、すでに動く何かに半分ほど近づいたような感覚になる
    この Signal 提案が、リアクティブ UI フレームワークのカンブリア爆発の中で似た役割を果たしてくれるなら賛成だ。さらに、リアクティビティが UI の外へ広がる助けにもなり得る。UI 状態ではないもののための漸進的な再計算状態ツリーを、しばしば想像してきた

    • Promises は主に async/await を追加するために入ったものだと見ていたし、実際に生活の質を大きく上げたのはそちらだ。実務では new Promise を直接書くことはまれだ
      初期の Promise の .then はネストした delegate より大きな改善で、単純なチェーンにはよいが、条件によって異なる Promise をチェーンしたり、特定のチェーンごとにエラー処理が違ったり、早期 return が必要になったりすると、コードはずっと読みづらく扱いにくくなり得る
      async/await を使えば、Promise ではないかのように呼び出しを書けるし、特定の Promise 呼び出しの周囲に try/catch を簡単に置け、早期 return も自然にできる
  • なぜこれが言語の一部になるべきなのか分からない。ライブラリで実現できるし、すでにそうしたライブラリもある。小さいのでコードに含めても大きな負担ではなく、言語に追加すること自体が目的になってはいけない
    現在の JS UI ライブラリが signals をあまりにうまく設計しているから言語の一部になるべきだ、と考えるのは傲慢だ。signals には異なるトレードオフを持つ実装が多くあり、そのどれも JavaScript 仕様で特別な位置を占める資格はない
    こうしたライブラリは signals を使う前は仮想 DOM を使っていた。幸い仮想 DOM は JS の一部にはならなかったが、signals は何が違うのか? 違わない。標準化すべきだという論拠は、仮想 DOM のときよりもさらに弱い
    Web を壊さずに、もはや望まれない機能を削除する方法が事実上ないランタイムに、流行しているものをすべて積み込むつもりなのか? かなり近視眼的だ

    • もっともな点はある。間違ったものは望まないが、正しいものは望んでいる
      リアクティブ UIは勝った。小さなアプリケーションでも状態管理をしていると複雑さが爆発することこそ、素の JS を使いにくくしている核心だ。自分にとっては、どんなリアクティブフレームワークでも素の JS よりましであり、そうだとすれば欠けている構成要素があるのかもしれない
      そろそろ 10 年ほど経ったので、標準化できる境界点を考えてみる時期だ。Promise のようにきちんとできれば、非常によくあるユースケースの複雑さを下げられる
      よりよい評価は「既存のリアクティブフレームワークはこの提案を使うだろうか?」と問うことだ。そうでないなら、なぜか、何が足りないのか、何が不要なのか、他言語の UI とリアクティビティから何を学べるのかを見るべきだ。散在した経験を精製する価値はある
    • Signals を標準化する良い理由の一つは、デバッグが悪夢のように見えることだ。計算済み signal の深いツリーが互いを連鎖的に発火させ、その連鎖反応の起点を探さなければならない状況を想像すればよい。標準化されれば、開発者ツールをその周辺に作れる
    • 標準ライブラリの大半について同じことが言える。ただし動機で述べられているように、JS の比較的小さな標準ライブラリを拡張し、よくある作業ごとにパッケージを取り込まなくてもよいようにする流れがある
      その必要性は議論できるが、標準ライブラリを拡張するなら、人気のあるものを見るのは良いアプローチだと思う
      signals は仮想 DOM の代替物ではない
    • Observable 提案を思い出す
  • アプリケーション全体に何かを通知したいときはイベントを使う
    window.dispatchEvent(new Event('counterChange'));
    そして反応したいアプリケーションのどの部分でも、こう購読できる
    window.addEventListener('counterChange', () => { ... do something ... });
    このやり方の何が問題なのか?

    • 歴史的に、この例こそ Web が jQuery へ進化し、そこから Angular と React の世界へ分かれていった理由だ
      イベント処理はとても簡単に散らかる。さらに深く見るなら、イベントバブリングと伝播を見ればよい
      大きなアプリケーションには堅牢なイベント処理が必要で、これが Angular や Vue のようなフレームワークの、今ではあまり表に出ない利点だ
      フレームワークなしに標準のイベント処理 API をそのまま使いたいとは思わないだろう。多数の要素について追加、削除、複製、発火、除去、1 回だけの発火などを扱っていると、深刻な望ましくない副作用が起こり得る
    • 本文によると、イベント発行者/Observableは複数回呼び出されると不要な作業を引き起こす
      signals との違いは、最終的な消費者が値を読むときにだけ結果値が計算される点にある。signal に実際に書き込む時点と非同期レンダー更新の予約を分離し、ウォッチャーが実行する計算チェーンはレンダー中に一度だけ実行される
      signal で送られた中間値は消えるので、その中で興味深いことを多く行うのは難しく、実質的にはレンダリング周期を調整するための高度な抽象化レイヤーに近い
    • Signals も結局は発行/購読だが、API はより使いやすい。リスナーが自動的に追加・解除されるからだ
      性能もより良い可能性がある。例えば 2 つの値に依存する計算があるとして、result = a ? b : 0 で a が偽なら、b が変わっても再計算する必要はない。signals ではこれが自動で行われるが、従来の発行/購読ではかなり多くのコードが必要になる
    • こうしたパターンを 10 年以上使ってきた。難しいのは、時間がたつとあるリスナーが別のイベントをトリガーし、さらに別のイベントが最初のルーチンに戻って、終わらないリスナーループが生じ得ることだ
      すべてのリスナーがそうした連鎖トリガーを作らないと保証するのも難しい
    • その方式には、提案書が強調している発行/購読アーキテクチャの欠点がすべてある
  • 何十年もの間、人々がなぜ状態の追跡と DOM 更新をそんなに難しがるのか理解しようとしてきた
    もちろん多少の規律は必要だが、数年ごとに出てくる解決策よりはずっと単純に感じる。Backbone、Knockout、Angular、React、言語自体の修正などがそうで、おそらく自分の考え方が根本的に違うのだと思う
    関数名にもそれが表れている。innerTextを更新することを「render」と呼んでいるが、実際にレンダリングしているわけではない。せいぜいブラウザがレンダリングしているのであって、それはペイントに関わる他のすべての処理も同じだ。最も単純な DOM 関数の一つを複雑にしようとする必死の試みに見えて、本当に困惑する

    • 単純なアプリケーションでは簡単
      より複雑になると簡単ではない
    • 「多少の規律は必要だ」という言葉から、昔なら ASM プログラマーとして狂ったポータブル C プログラマーたちに腹を立て、その後は C プログラマーとして狂ったメモリ安全な Java プログラマーたちに腹を立てていただろうという強い感じがする
      プログラミングの進歩は、良い結果を得るために必要な儀式的で厳格な規律を取り除いていく過程と見ることができる
      React が次の進化だという意味ではないが、signals は明らかに正しい方向への一歩だ
    • 何十年というのは長い時間だ。ブラウザごとに DOM 更新がどれほど複雑だったか覚えているはず
      DOM をデータ状態と同期させること自体はそれほど難しくないが、それを60fpsで非常に高性能に行うのはものすごく難しい。特に、漏れがなく、かつ面倒すぎない API を作る場合はなおさらだ
      変更を生きた DOM ツリーに変換して反映するより、正直なところゲームのように canvas にピクセルを描く方が簡単かもしれない
    • 数十万行規模のFX 取引アプリケーションを作っている。太いデスクトップアプリを置き換えるレベルで、20〜30人の開発者と、それぞれ開発者を抱える複数の顧客がいる。フレームワークなしでやってみろと言われたら、幸運を祈るしかない
    • まったく同感。非常に複雑でインタラクションの多い SPA も開発したことがあるが、こうしたものが解決するとされる問題に、まだ直面したことがない
  • Promises は良い成功例だが、async/await がなければ必ずしも標準化する必要はなかった
    現在の草案は Angular、Bubble、Ember、FAST、MobX、Preact、Qwik、RxJS、Solid、Starbeam、Svelte、Vue、Wiz などの作者/メンテナーの設計を基にしているというが、既存ライブラリの作者たちがこの提案をどう見ているのか気になる。React がリストにないのも興味深い
    Signals はチャンネルに少し似ているが、単一の受信者ではなくブロードキャストである点が違う。これを活用して Web Worker が onMessage コールバックではなくチャンネルで通信できるようになると素晴らしそうだ。特に Go のように signals/channels/promises の上で select できるなら、複数の同時メッセージング機構をコールバックで管理するより構文上の利点がある。たとえば signals を Promise.any に含められるようにする、といった形だ

    • 「async/await がなければ標準化する必要はなかった」という点には強く反対する
      x instanceof Promise は単純に機能しない。自分のライブラリの then メソッドは catch コールバックを受け取るが、別のライブラリは受け取らないとしたら、静かに相互運用できず、検出する方法もない。finally はいつ実行されるのか? コールバックがどの程度非同期に実行されると期待できるのか?
      標準がなければ、Promise を使うすべてのライブラリが独自のポリフィルを持ち込まなければならない。すでに存在するものを信用できないからだ。そして、他のライブラリの Promise も実際には消費できない。期待した通りに動作すると信じられないからだ
      これは推測ではなく、何年もの間実際にそうで、多くの人が耐えなければならなかった地獄だった
    • async/await とは無関係な標準化の利点もある。JavaScript エンジンが Promise を多用するアプリケーションに恩恵のある性能最適化を行えるようになり、標準でなければそれは不可能だったはずだ
    • React がリストにない理由は、signals が Preact とは違ってReact のコア APIの一部ではないからだ
      ぼんやりした直感では、signals は一般化された useEffect() にあまりにも似ていて、React に入るとレンダーサイクル中に何が起きるのかをさらに混乱させそうに思える。良くも悪くも React は signals とは異なる更新方式を選んだ。ただし、適用可能性については自分が間違っているかもしれない
    • React がこのリストにない理由は、その効果が命令型ではなく宣言型だからだ。props の変更と再レンダーは、一段抽象化された宣言型と見ることもできる。useEffect は命令型の動作をきれいに隔離する
      これは Ember のデータバインディングに非常によく似て見え、結局は命令型の悪夢になり得る。デフォルトの状態が「自分の足を撃つ銃」に近く、そうならないようにするには莫大な認知負荷とメタパターンが必要になる
    • これは「EventEmitter」と呼べばいいのかもしれない
      https://nodejs.org/en/learn/asynchronous-work/the-nodejs-eve...
  • リンク先 README の例が理解できなかった
    // A library or framework defines effects based on other Signal primitives
    declare function effect(cb: () => void): (() => void);
    どのライブラリ?どのフレームワーク?ここで迷子になった。effect とは何なのか?
    effect(() => element.innerText = parity.get());
    effect は、parity が変わるたびにこのラムダを呼び出すべきだとどうやって知るのか?signal が変わるたびにこのラムダを呼び出すのか?だとしたらキャッシュの話はなぜ出てくるのか?たぶんそうではないのだろう。
    いずれにせよ、著者たちが伝えようとしていたことを正しく理解できているなら、signal というアイデア自体は妥当に見える。ただ、このような分離アーキテクチャの大きな問題は、アプリケーションが十分に複雑になると、特定のイベントがなぜ発生しているのか追っているうちに迷子になることだ。理想的には、signals がスタックトレースを修正し、コールバックが呼び出されたときに、そもそも signal をトリガーしたコードのスタックトレースがすでに含まれているべきだ。

    • effect という関数をエクスポートするライブラリはいくつもあり、signal の更新に反応して任意のコードを実行できるようにしている。Preact ドキュメントの signals と effects の入門がよい: https://preactjs.com/guide/v10/signals#effectfn
      私の理解では、このような effect 関数はまずコールバックを一度実行し、その実行中にどの signal にアクセスしたかを確認して、そのコールバックが依存する signal が更新されるたびにコールバックを再度呼び出す。signal へのアクセスが同期的でシングルスレッドなら、コールバック実行中に signal にアクセスしたという事実だけで、そのコールバックがその signal を購読すべきだと分かる。
      getter でも可能だ。effect 関数が getter メソッドで signal のどのプロパティにアクセスしたかを追跡する方式で、Vue 2 は以前この方式を使っていたと理解している。プロキシでオブジェクトアクセスを追跡することもできる。提案書の例には signal の値にアクセスするために呼び出す get メソッドがあり、このメソッドの実行を通じて依存関係を追跡できる。
      [1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
      [2] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
    • parity.get() の呼び出しが、effect() に渡された関数に対する依存関係を登録する。parity が更新されると、その関数が呼び出される。
      signal が変わるたびに呼び出すのではなく、依存している signal が変わったときだけ呼び出す。
      この場合 parityisEven に依存し、isEvencounter に依存している。なので counter が更新されると依存関係チェーン全体が無効化され、parity が無効化されてコールバックが再実行される。
    • signals の実装は、名前が何であれ、おおむね動的依存グラフを作り、ノードを読むとエッジができる。この仮想的な effect のような追跡コンテキストでは、読み取りが signal の状態ノードと effect の計算ノードの間にエッジを作り、実質的に後者が前者の以後の書き込みを購読することで、いつ計算を再実行するかを決める。
    • effect は呼び出したい任意の関数だ。
      signals では、依存関係追跡メカニズムがどの値を再計算すべきかを把握し、その結果としてシステムはどの関数を再呼び出しすべきかも分かるようになる。
    • watcher が effect の実装に必要になりそうだ。
  • 関連して S.js がある: https://github.com/adamhaile/s
    signals は好きだ。UI を作るときは他のどんなプリミティブよりも好んで使うし、例外があるとすれば cassowary 制約アルゴリズムくらいかもしれない。趣味で使うあらゆる言語で signals をまねしてみようとしている。
    しかし、JavaScript 言語そのものに入れるものではまったくないと思う。しばらく言語を放っておいてほしい。人々はすでについていくのに苦労しているし、TC-39 はすでに人々を言語から怖がらせて遠ざけている。

  • これは私がいちばん好きな JS effect システムである MobX にとても似て見える。
    MobX 版はこうだ。
    import { observable, computed, autorun } from 'mobx';
    const counter = observable.box(0);
    const isEven = computed(() => (counter.get() & 1) === 0);
    const parity = computed(() => isEven.get() ? "even" : "odd");
    autorun(() => { element.innerText = parity.get(); });
    setInterval(() => counter.set(counter.get() + 1), 1000);

    • MobX は signals で合っている。ただし依存関係を getter で明示的に追跡する代わりに、プロキシオブジェクトを通じて暗黙的に追跡する signals だ。
  • 「自分が最近使っているフレームワークを標準ライブラリに焼き込もう!」という感じだ。
    彼女の名前を体にタトゥーで入れるようなものだ。

    • これはそういうものではない。
      ほとんどのフレームワークが使うように収束してきた構成要素を標準ライブラリに入れるものだ。
      Promises も広く使われた後に標準ライブラリに入り、これもそれと似ている。