Signals Deep Dive

641 Views

June 05, 26

スライド概要

フロントエンド・PHPカンファレンス北海道

profile-image

Angularちょっとできる

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

(ダウンロード不可)

関連スライド

各ページのテキスト
1.

Signals Deep Dive フロントエンド・PHPカンファレンス北海道 2026 nishitaku

2.

はじめまして、nishitakuです 西濃 拓郎(にしの たくろう) フリーランスエンジニア 2

3.

Signals 知ってる人〜 (Vue.jsのref、SvelteのRuneなど含む) 3

4.

イントロダクション Deep Dive の価値 想定外の挙動にであったとき、「なぜそうなるか」を理解できる これから話すこと Signals がどういう仕組みで どうやって必要な箇所だけを更新しているのか 持ち帰ってもらえること 現代のリアクティブシステムの勘所を掴む 4

5.

目次 1. Signals 概要 2. Signals の特徴 特徴① 依存関係の自動追跡 特徴② Push-Pull ハイブリッド方式による遅延評価 特徴③ メモ化 3. Signal Polyfill の実装を読む 5

6.

Signals 概要

7.
[beta]
Signals 概要
const counter = new Signal.State(0);
const isEven = new Signal.Computed(() => (counter.get() & 1) == 0);
effect(() => { element.innerText = isEven.get() });

状態と依存関係を効率的に扱うリアクティブモデル
各種フレームワーク(Angular/Preact/Svelte/Vue ...etc)で採用
基本3要素
State: 手動で設定される値
Computed: Stateに依存して計算される値
Effect: StateやComputedに依存して実行されるコールバック
TC39 proposal-signals (Stage 1)

7

8.

Signals の特徴① 依存関係の自動追跡

9.
[beta]
Vanilla JSの場合
let counter = 0;
const setCounter = (value) => {
counter = value;
render();
};
const isEven = () => (counter & 1) == 0;
const parity = () => isEven() ? "even" : "odd";
const render = () => element.innerText = parity();
setInterval(() => setCounter(counter + 1), 1000);

開発者が依存関係を知っておく必要がある
→ 変更時の対応漏れや過剰更新が問題になり、スケールしない 9

10.
[beta]
Signals の場合
const counter = new Signal.State(0);
const isEven = new Signal.Computed(() => (counter.get() & 1) == 0);
const parity = new Signal.Computed(() => isEven.get() ? "even" : "odd");
effect(() => {
element.innerText = parity.get();
});
setInterval(() => counter.set(counter.get() + 1), 1000);

依存関係を自動で追跡してくれる
→ 依存関係が増えてもスケールしやすい
10

11.
[beta]
宣言的UIの場合
// JavaScript
let name = "太郎";
let age = 20;
const validatedName = () => f(name); // nameに依存
const parity = () => (age % 2 === 0 ? '偶数' : '奇数'); // ageに依存
// HTML
<p>名前は{{ validatedName() }}です</p>
<p>年齢は{{ parity() }}です。</p>

依存関係がわからないと、
だけでなく
parity

age

が変更されたときに
も再計算する必要がある

validatedName

11

12.
[beta]
Signals の場合
// JavaScript
const name = signal("太郎");
const age = signal(20);
const validatedName = computed(() => f(name()));
const parity = () => (age() % 2 === 0 ? '偶数' : '奇数');
// HTML
<p>名前は{{ validatedName() }}です</p>
<p>年齢は{{ parity() }}です。</p>

効率的に必要な計算だけを再実行できる
12

13.

Signals を使うことで 自動で依存関係を追跡できる

14.

Signals の特徴② Push-Pull ハイブリッド方式による遅延評価

15.

データを誰が主導して渡すか Push型:データを持っている側が通知する 例)Pub/Sub、WebSocket、Observerパターン、EventEmitter 常に最新状態 無駄な再計算が発生しやすい Pull型:データを使う側が取りにいく 例)ポーリング、getter、関数呼び出し 必要なときに取得して計算 毎回取得コストが発生してしまう 15

16.

Signals は Push-Pull ハイブリッド方式 const counter = new Signal.State(0); const isEven = new Signal.Computed(() => (counter.get() & 1) == 0); Stateの変更は即座に通知されるPush型 Computedの評価はPull型 16

17.

Signals の特徴③ メモ化

18.

Computed のメモ化 const parity = () => (age() % 2 === 0 ? '偶数' : '奇数'); 依存先のdirtyフラグをキャッシュキーとしている age age のdirtyフラグが のdirtyフラグが true false → 再計算した値を返す → キャッシュ値を返す 18

19.

Signal Polyfill の実装を読む

20.
[beta]
Signal Polyfill とは

TC39 proposal-signals のポリフィル実装
import { Signal } from "signal-polyfill";
import { effect } from "./effect.js";
const counter = new Signal.State(0);
const isEven = new Signal.Computed(() => (counter.get() & 1) == 0);
const parity = new Signal.Computed(() => (isEven.get() ? "even" : "odd"));
effect(() => console.log(parity.get())); // Console logs "even" immediately.
setInterval(() => counter.set(counter.get() + 1), 1000); // Changes the counter every 1000ms.

// effect triggers console log "odd"
// effect triggers console log "even"
// effect triggers console log "odd"
// ...

20

21.

依存関係の追跡を実現するための3ステップ 依存グラフの構築 読むことで依存を登録する Push 「古くなった」を伝播する Pull 必要になったときだけ再計算する 21

22.
[beta]
const counter = new Signal.State(0);

依存グラフの構築

const parity = new Signal.Computed(() => (counter.get() % 2) == 0 ? "even" : "odd");
effect(() => { element.innerText = parity.get() });

Computed(parity)

runtime

State(counter)

activeConsumer = parity
counter.get()
producerAccessed(counter)
subscribers.add(parity)
dependencies.add(counter)
activeConsumer = null

Computed(parity)

runtime

State(counter)

22

23.
[beta]
const counter = new Signal.State(0);

Pushフェーズ
effect

const parity = new Signal.Computed(() => (counter.get() % 2) == 0 ? "even" : "odd");
effect(() => { element.innerText = parity.get() });

Computed(parity)

State(counter)

App

counter.set(1)
mark dirty
dirty = true

mark dirty
dirty = true

effect

Computed(parity)

State(counter)

App

23

24.
[beta]
const counter = new Signal.State(0);

Pullフェーズ

const parity = new Signal.Computed(() => (counter.get() % 2) == 0 ? "even" : "odd");
effect(() => { element.innerText = parity.get() });

effect

Computed(parity)

parity.get()
alt

[dirty]
recompute

value

effect

Computed(parity)

24

25.

Signal Polyfill の実装 依存関係の追跡は、双方向の依存グラフによって実現 データを保持する側 は データを取得する側 は producer.subscribers に に consumer.dependencies consumer を登録 を登録 producer 25

26.

Signals Deep Dive のまとめ Signals は「誰が誰を読んだか」の依存グラフを構築する 依存グラフにより変更伝搬を自動化する Push-Pull ハイブリッド方式により必要時だけ再計算する 26

27.

ご清聴ありがとうございました nishitaku